commit
c0c8734e14
178 changed files with 12059 additions and 0 deletions
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
### gradle ### |
||||
.gradle |
||||
!gradle/wrapper/gradle-wrapper.jar |
||||
|
||||
### STS ### |
||||
.settings/ |
||||
.apt_generated |
||||
.classpath |
||||
.factorypath |
||||
.project |
||||
.settings |
||||
.springBeans |
||||
bin/ |
||||
|
||||
### IntelliJ IDEA ### |
||||
.idea |
||||
*.iws |
||||
*.iml |
||||
*.ipr |
||||
rebel.xml |
||||
|
||||
### NetBeans ### |
||||
nbproject/private/ |
||||
nbbuild/ |
||||
nbdist/ |
||||
.nb-gradle/ |
||||
|
||||
### maven ### |
||||
target/ |
||||
*.war |
||||
*.ear |
||||
*.zip |
||||
*.tar |
||||
*.tar.gz |
||||
*.versionsBackup |
||||
|
||||
### vscode ### |
||||
.vscode |
||||
|
||||
### logs ### |
||||
/logs/ |
||||
*.log |
||||
|
||||
### temp ignore ### |
||||
*.cache |
||||
*.diff |
||||
*.patch |
||||
*.tmp |
||||
*.java~ |
||||
*.properties~ |
||||
*.xml~ |
||||
|
||||
### system ignore ### |
||||
.DS_Store |
||||
Thumbs.db |
||||
Servers |
||||
.metadata |
||||
|
||||
# ui ignore |
||||
kicc-ui/ |
@ -0,0 +1,107 @@
@@ -0,0 +1,107 @@
|
||||
<h1 align="center"> |
||||
<b> |
||||
<a href="https://godolphinx.org"><img src="https://godolphinx.org/images/dolphin-platform-logo.svg" /></a><br> |
||||
</b> |
||||
</h1> |
||||
|
||||
<p align="center"> 一个快速开发软件的平台 </p> |
||||
|
||||
<p align="center"> |
||||
<a href="https://godolphinx.org/"><b>Website</b></a> • |
||||
<a href="https://godolphinx.org/microservice/description.html"><b>Documentation</b></a> |
||||
</p> |
||||
|
||||
<div align="center"> |
||||
<a href="https://godolphinx.org"> |
||||
<img src="https://img.shields.io/npm/l/vue.svg?sanitize=true"> |
||||
</a> |
||||
<a href="https://gitpod.io/#https://github.com/wangxiang4/dolphin"> |
||||
<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod&style=flat-square"> |
||||
</a> |
||||
<a href="https://discord.gg/DREuQWrRYQ"> |
||||
<img src="https://img.shields.io/badge/chat-on%20discord-7289da.svg?sanitize=true"/> |
||||
</a> |
||||
</div> |
||||
|
||||
## 🐬 介绍 |
||||
海豚生态计划-打造一个web端,安卓端,ios端的一个海豚开发平台生态圈,不接收任何商业化,并且完全免费开源(包含高级功能)。 |
||||
|
||||
## 💪 愿景 |
||||
让人人都可以快速高效的开发软件 |
||||
|
||||
## ✨ 特性 |
||||
- 主体框架:采用最新的`Spring Cloud 2021.0.1`, `Spring Boot 2.6.4`, `Spring Cloud Alibaba 2021.1`版本进行系统设计。 |
||||
- 统一注册:支持`Nacos`作为注册中心,实现多配置、分群组、分命名空间、多业务模块的注册和发现功能。 |
||||
- 统一认证:统一`Oauth2`认证协议,并支持自定义grant_type实现手机号码登录,第三方登录集成JustAuth实现微信、支付宝等多种登录模式。 |
||||
- 业务监控:利用`Spring Boot Admin`来监控各个独立微服务运行状态。 |
||||
- 内部调用:集成了`Feign`与自定义内部注解,支持内部调用。 |
||||
- 业务熔断:采用`Sentinel`实现业务熔断处理,避免服务之间出现雪崩。 |
||||
- 在线文档:通过接入`Knife4j`,实现在线API文档的查看与调试。 |
||||
- 业务分离:采用前后端分离的框架设计,提高开发效率、降低维护成本、增强系统稳定性和灵活性。 |
||||
- 多租户功能:集成`Mybatis Plus`,自定义sql执行拦截器,实现SAAS多租户。 |
||||
- 消息中间件:采用`RocketMQ`,实现服务之间消息转发。 |
||||
- 分布式事物方案:采用`seata`,实现多个微服务分布式事物一致。 |
||||
- 分布式定时器:采用`XXL-JOB`,实现多个微服务分布式任务调度。 |
||||
- 微服务网关:采用`Spring Gateway`实现流量配置动态化、API管理和路由、负载均衡和容错、解决跨域问题、鉴权,限流,熔断,防火墙等等。 |
||||
|
||||
## <img width="28" style="vertical-align:middle" src="https://godolphinx.org/images/hacktoberfest-logo.svg"> 黑客节 |
||||
加入[Github HackToberFest](https://hacktoberfest.com/) 开始为此项目做出贡献. |
||||
|
||||
## 🔨 开发目录 |
||||
|
||||
``` |
||||
├─dolphin -- 父项目,各模块分离,方便微服务扩展 |
||||
│ ├─doc -- 文档数据-包含项目的一些数据资料 |
||||
│ ├─docker-cloud -- docker-compose容器配置 |
||||
│ ├─dolphin-auth -- 认证授权中心,基于 spring security oAuth2 |
||||
│ ├─dolphin-common -- 公共通用模块,主模块 |
||||
│ │ ├─dolphin-common-bom -- 全局jar BOM标准定义 |
||||
│ │ ├─dolphin-common-core -- 公共工具类核心包 |
||||
│ │ ├─dolphin-common-data -- 数据服务核心包 |
||||
│ │ ├─dolphin-common-datasource -- 动态切换数据源组件 |
||||
│ │ ├─dolphin-common-feign -- feign-sentinel服务降级熔断、限流组件 |
||||
│ │ ├─dolphin-common-job -- 定时任务,基于xxl-job |
||||
│ │ ├─dolphin-common-log -- 日志服务 |
||||
│ │ ├─dolphin-common-mock -- 单元模拟测试工具类 |
||||
│ │ ├─dolphin-common-rocketmq -- 阿里 rocketmq 消息中间件 |
||||
│ │ ├─dolphin-common-seata -- 阿里巴巴-seata分布式事务解决方案 |
||||
│ │ ├─dolphin-common-security -- 安全工具类 |
||||
│ │ ├─dolphin-common-swagger -- 接口文档 |
||||
│ │─dolphin-common-demo -- 组件使用案列 |
||||
│ │ ├─dolphin-common-demo-mq -- 消息中心间演示 |
||||
│ │ ├─dolphin-common-demo-seata -- 分布式事务解决方案演示 |
||||
│ │─dolphin-gateway -- 服务网关,基于 spring cloud gateway |
||||
│ │─ddolphin-platform -- 微服务平台 |
||||
│ │ ├─dolphin-platform-api -- 微服务api调用(添加调用的微服务api依赖库,实现调用) |
||||
│ │ │ ├─dolphin-common-api -- 通用业务模块公共api模块 |
||||
│ │ │ ├─dolphin-monitor-api -- 运维监控api模块 |
||||
│ │ │ ├─dolphin-system-api -- 系统api模块 |
||||
│ │ │ ├─dolphin-template-api -- 新建api模块模板,只提供基础依赖 |
||||
│ │ ├─dolphin-platform-biz -- 微服务业务模块 |
||||
│ │ │ ├─dolphin-common-biz -- 通用业务模块 |
||||
│ │ │ ├─dolphin-monitor-biz -- 运维监控业务模块 |
||||
│ │ │ ├─dolphin-system-biz -- 系统业务模块 |
||||
│ │ │ ├─dolphin-template-biz -- 新建业务模块模板,只提供基础依赖 |
||||
│ │─dolphin-register -- 注册配置中心 |
||||
│ │─dolphin-visual 可视化图形界面 |
||||
│ │ ├─dolphin-rocketmq-dashboard -- RocketMQ可视化监控平台 |
||||
│ │ ├─dolphin-sentinel-dashboard -- 哨兵流量控制可视化平台 |
||||
│ │ ├─dolphin-spring-dashboard -- SpringBoot可视化监控平台 |
||||
│ │ ├─dolphin-xxl-job-admin -- XXL-JOB可视化监控平台 |
||||
``` |
||||
|
||||
## 🤔 一起讨论 |
||||
加入我们的 [Discord](https://discord.gg/DREuQWrRYQ) 开始与大家交流。 |
||||
|
||||
## 🤗 我想成为开发团队的一员! |
||||
欢迎😀!我们正在寻找有才华的开发者加入我们,让海豚开发平台变得更好!如果您想加入开发团队,请联系我们,非常欢迎您加入我们!💖 |
||||
|
||||
## 在线一键设置 |
||||
您可以使用 Gitpod,一个在线 IDE(开源免费)来在线贡献或运行示例。 |
||||
|
||||
[](https://gitpod.io/#https://github.com/wangxiang4/dolphin) |
||||
|
||||
## 📄 执照 |
||||
[Dolphin Development Platform 是获得MIT许可](https://github.com/wangxiang4/dolphin/blob/master/LICENSE) 的开源软件 。 |
||||
|
||||
|
@ -0,0 +1,276 @@
@@ -0,0 +1,276 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
|
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-bom</artifactId> |
||||
<version>1.0.0</version> |
||||
<packaging>pom</packaging> |
||||
|
||||
<name>kicc-common-bom</name> |
||||
<description>全局jar BOM标准定义(可以设置工程内部的jar的标准,也可以设置第三方依赖jar的标准)</description> |
||||
|
||||
<properties> |
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
||||
<maven.compiler.source>1.8</maven.compiler.source> |
||||
<maven.compiler.target>1.8</maven.compiler.target> |
||||
<kicc.common.version>${project.version}</kicc.common.version> |
||||
<spring-boot.version>2.6.3</spring-boot.version> |
||||
<security.oauth.version>2.1.8.RELEASE</security.oauth.version> |
||||
<log4j2.version>2.17.1</log4j2.version> |
||||
<fastjson.version>1.2.78</fastjson.version> |
||||
<swagger.core.version>1.5.24</swagger.core.version> |
||||
<mybatis-plus.version>3.5.1</mybatis-plus.version> |
||||
<sms.version>1.2.0</sms.version> |
||||
<git.commit.plugin>4.9.9</git.commit.plugin> |
||||
<spring.checkstyle.plugin>0.0.29</spring.checkstyle.plugin> |
||||
<captcha.version>2.2.1</captcha.version> |
||||
<oss.version>1.0.3</oss.version> |
||||
<excel.version>1.1.1</excel.version> |
||||
<velocity.version>2.3</velocity.version> |
||||
<flowable.version>6.4.2</flowable.version> |
||||
<liquibase.version>4.22.0</liquibase.version> |
||||
<ureport2.version>2.2.9</ureport2.version> |
||||
</properties> |
||||
|
||||
<!-- 定义全局jar版本,模块使用需要再次引入但不用写版本号--> |
||||
<dependencyManagement> |
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-core</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-data</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-datasource</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-feign</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-rocketmq</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-seata</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-job</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-log</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-mock</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-security</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-swagger</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-api</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-system-api</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-monitor-api</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-report-api</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-workflow-api</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-template-api</artifactId> |
||||
<version>${kicc.common.version}</version> |
||||
</dependency> |
||||
<!--稳定版本,替代spring security2.6.3 bom内置--> |
||||
<dependency> |
||||
<groupId>org.springframework.security.oauth.boot</groupId> |
||||
<artifactId>spring-security-oauth2-autoconfigure</artifactId> |
||||
<version>${security.oauth.version}</version> |
||||
</dependency> |
||||
<!--swagger 最新依赖内置版本--> |
||||
<dependency> |
||||
<groupId>io.swagger</groupId> |
||||
<artifactId>swagger-models</artifactId> |
||||
<version>${swagger.core.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>io.swagger</groupId> |
||||
<artifactId>swagger-annotations</artifactId> |
||||
<version>${swagger.core.version}</version> |
||||
</dependency> |
||||
<!--fastjson 版本--> |
||||
<dependency> |
||||
<groupId>com.alibaba</groupId> |
||||
<artifactId>fastjson</artifactId> |
||||
<version>${fastjson.version}</version> |
||||
</dependency> |
||||
<!--阿里云短信服务--> |
||||
<dependency> |
||||
<groupId>cn.javaer.aliyun</groupId> |
||||
<artifactId>aliyun-spring-boot-starter-sms</artifactId> |
||||
<version>${sms.version}</version> |
||||
</dependency> |
||||
<!--orm mybatis-plus相关--> |
||||
<dependency> |
||||
<groupId>com.baomidou</groupId> |
||||
<artifactId>mybatis-plus-boot-starter</artifactId> |
||||
<version>${mybatis-plus.version}</version> |
||||
</dependency> |
||||
<!--mybatis-generator--> |
||||
<dependency> |
||||
<groupId>com.baomidou</groupId> |
||||
<artifactId>mybatis-plus-generator</artifactId> |
||||
<version>${mybatis-plus.version}</version> |
||||
</dependency> |
||||
<!--velocity模板引擎生成简单crud代码--> |
||||
<dependency> |
||||
<groupId>org.apache.velocity</groupId> |
||||
<artifactId>velocity-engine-core</artifactId> |
||||
<version>${velocity.version}</version> |
||||
</dependency> |
||||
<!--web 模块--> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-web</artifactId> |
||||
<version>${spring-boot.version}</version> |
||||
<exclusions> |
||||
<!--排除tomcat依赖,使用undertow容器--> |
||||
<exclusion> |
||||
<artifactId>spring-boot-starter-tomcat</artifactId> |
||||
<groupId>org.springframework.boot</groupId> |
||||
</exclusion> |
||||
</exclusions> |
||||
</dependency> |
||||
<!--指定 log4j 版本--> |
||||
<dependency> |
||||
<groupId>org.apache.logging.log4j</groupId> |
||||
<artifactId>log4j-bom</artifactId> |
||||
<version>${log4j2.version}</version> |
||||
<type>pom</type> |
||||
<scope>import</scope> |
||||
</dependency> |
||||
<!--oss文件管理,采用S3协议封装,兼容阿里云OSS,腾讯云COS,七牛云,京东云,minio 等不需要引入对应厂商的SDK就可以使用厂商的OSS服务 |
||||
代码开源地址: https://github.com/pig-mesh/oss-spring-boot-starter --> |
||||
<dependency> |
||||
<groupId>com.pig4cloud.plugin</groupId> |
||||
<artifactId>oss-spring-boot-starter</artifactId> |
||||
<version>${oss.version}</version> |
||||
</dependency> |
||||
<!-- 验证码处理支持很多种验证码风格基于google的aviator计算引擎实现,性能比较高效 |
||||
代码开源地址: https://github.com/pig-mesh/easy-captcha --> |
||||
<dependency> |
||||
<groupId>com.pig4cloud.plugin</groupId> |
||||
<artifactId>captcha-spring-boot-starter</artifactId> |
||||
<version>${captcha.version}</version> |
||||
</dependency> |
||||
<!-- excel 导入导出处理,基于alibaba的EasyExcel实现,读取大文件不怎么吃内存,性能比较高效 |
||||
代码开源地址: https://github.com/pig-mesh/excel-spring-boot-starter --> |
||||
<dependency> |
||||
<groupId>com.pig4cloud.excel</groupId> |
||||
<artifactId>excel-spring-boot-starter</artifactId> |
||||
<version>${excel.version}</version> |
||||
</dependency> |
||||
<!-- flowable-SpringBoot依赖 --> |
||||
<dependency> |
||||
<groupId>org.flowable</groupId> |
||||
<artifactId>flowable-spring-boot-starter-process-rest</artifactId> |
||||
<version>${flowable.version}</version> |
||||
</dependency> |
||||
<!-- |
||||
引入建模设计器api请求,具体我也查看了内部是依赖了一个flowable-ui-modeler-logic做核心的逻辑操作 |
||||
具体实现: https://github.com/flowable/flowable-engine/tree/main/modules/flowable-ui/flowable-ui-modeler-rest |
||||
因为Flowable6.5以后开始收费,所以后期打算用camunda,对应的我也去查看了一遍camunda的源代码,发现对应也有此功能,camunda具体实现 |
||||
是采用了一个engine-rest工程做的api请求,但是区别是camunda是把所有的api请求放在一起了,并没有像flowable分模块功能实现的api请求 |
||||
比如我只想要导入建模设计器api请求,如果使用camunda就不行了只能把所有的api请求全部导入,camunda内部建模这块的实现也是引用了一个 |
||||
camunda-bpmn-model工程做核心的逻辑操作,具体引用是在bom.xml引入 |
||||
具体实现: https://github.com/camunda/camunda-bpm-platform/blob/master/bom/pom.xml |
||||
具体实现: https://github.com/camunda/camunda-bpm-platform/blob/master/engine-rest |
||||
所以此处可以使用rest工程,因为camunda也有对应的rest工程可以替换 |
||||
引入此包原因:查看内部源码发现官方写的比我们自己写出来的模型api接口要更加规范,包括预测未来会发生什么样的错误,比如内部保存采用 |
||||
了版本校验(如果此时你在修改模型时别人提交了你的模型版本,如果没有版本校验就会出问题),以及覆盖了绝大多数处理模型的应用场景,只需要调用 |
||||
这个包的接口就行了,不需要在自己去写一大把模型操作逻辑,提高开发效率 |
||||
--> |
||||
<dependency> |
||||
<groupId>org.flowable</groupId> |
||||
<artifactId>flowable-ui-modeler-rest</artifactId> |
||||
<version>${flowable.version}</version> |
||||
<exclusions> |
||||
<exclusion> |
||||
<artifactId>spring-boot-starter-log4j2</artifactId> |
||||
<groupId>org.springframework.boot</groupId> |
||||
</exclusion> |
||||
</exclusions> |
||||
</dependency> |
||||
<!-- 数据库重构工具:构建flowable-ui-modeler-rest数据库 --> |
||||
<dependency> |
||||
<groupId>org.liquibase</groupId> |
||||
<artifactId>liquibase-core</artifactId> |
||||
<version>${liquibase.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.bstek.ureport</groupId> |
||||
<artifactId>ureport2-console</artifactId> |
||||
<version>${ureport2.version}</version> |
||||
</dependency> |
||||
</dependencies> |
||||
</dependencyManagement> |
||||
|
||||
<build> |
||||
<plugins> |
||||
<!--maven打包插件(打jar包会在classes生成git.properties文件包含了目前git相关信息方便查看git环境错误)--> |
||||
<plugin> |
||||
<groupId>io.github.git-commit-id</groupId> |
||||
<artifactId>git-commit-id-maven-plugin</artifactId> |
||||
<version>${git.commit.plugin}</version> |
||||
</plugin> |
||||
<!--代码格式插件,默认使用spring 规则,跟前端EsLine一样保证代码规范--> |
||||
<plugin> |
||||
<groupId>io.spring.javaformat</groupId> |
||||
<artifactId>spring-javaformat-maven-plugin</artifactId> |
||||
<version>${spring.checkstyle.plugin}</version> |
||||
</plugin> |
||||
</plugins> |
||||
</build> |
||||
|
||||
</project> |
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-tool</artifactId> |
||||
<version>1.0.0</version> |
||||
</parent> |
||||
|
||||
<artifactId>kicc-common-core</artifactId> |
||||
<packaging>jar</packaging> |
||||
|
||||
<description>kicc 公共工具类核心包</description> |
||||
|
||||
<!--考虑这个作为一个单模块使用,后续引入依赖需要注意低耦合--> |
||||
<dependencies> |
||||
<!--hutool--> |
||||
<dependency> |
||||
<groupId>cn.hutool</groupId> |
||||
<artifactId>hutool-all</artifactId> |
||||
<version>${hutool.version}</version> |
||||
</dependency> |
||||
<!--server-api--> |
||||
<dependency> |
||||
<groupId>javax.servlet</groupId> |
||||
<artifactId>javax.servlet-api</artifactId> |
||||
</dependency> |
||||
<!--hibernate-validator--> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-validation</artifactId> |
||||
</dependency> |
||||
<!--json模块--> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-json</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework</groupId> |
||||
<artifactId>spring-webmvc</artifactId> |
||||
<scope>provided</scope> |
||||
</dependency> |
||||
<!--swagger 依赖支持注解--> |
||||
<dependency> |
||||
<groupId>io.swagger</groupId> |
||||
<artifactId>swagger-annotations</artifactId> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
package com.cloud.kicc.common.core.annotation; |
||||
|
||||
import com.cloud.kicc.common.core.config.JacksonAutoConfiguration; |
||||
import org.springframework.context.annotation.Import; |
||||
|
||||
import java.lang.annotation.*; |
||||
|
||||
/** |
||||
*<p> |
||||
* 激活 Jackson 自动转换配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/19 |
||||
*/ |
||||
@Target({ ElementType.TYPE }) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Documented |
||||
@Inherited |
||||
@Import({ JacksonAutoConfiguration.class }) |
||||
public @interface EnableKiccJacksonAutoConvert { |
||||
} |
@ -0,0 +1,114 @@
@@ -0,0 +1,114 @@
|
||||
package com.cloud.kicc.common.core.api; |
||||
|
||||
import io.swagger.annotations.ApiModelProperty; |
||||
import lombok.*; |
||||
import lombok.experimental.Accessors; |
||||
|
||||
import java.io.Serializable; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
*<p> |
||||
* 响应信息主体 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@ToString |
||||
@NoArgsConstructor |
||||
@AllArgsConstructor |
||||
@Accessors(chain = true) |
||||
public class R<T> implements Serializable { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
/** |
||||
* 成功标记 |
||||
*/ |
||||
public static Integer SUCCESS = 200; |
||||
/** |
||||
* 失败标记 |
||||
*/ |
||||
public static Integer FAIL = 500; |
||||
/** |
||||
* 未认证 |
||||
*/ |
||||
public static Integer UNAUTH = 401; |
||||
|
||||
@Getter |
||||
@Setter |
||||
@ApiModelProperty("状态编码") |
||||
private int code; |
||||
|
||||
@Getter |
||||
@Setter |
||||
@ApiModelProperty("提示消息") |
||||
private String msg; |
||||
|
||||
@Getter |
||||
@Setter |
||||
@ApiModelProperty("结果集数量统计") |
||||
private long total; |
||||
|
||||
@Getter |
||||
@Setter |
||||
@ApiModelProperty("结果集") |
||||
private T data; |
||||
|
||||
public static <T> R<T> ok() { |
||||
return restResult(null, SUCCESS, "成功"); |
||||
} |
||||
|
||||
public static <T> R<T> ok(T data) { |
||||
return restResult(data, SUCCESS, "成功"); |
||||
} |
||||
|
||||
public static <T> R<T> ok(T data, String msg) { |
||||
return restResult(data, SUCCESS, msg); |
||||
} |
||||
|
||||
public static <T> R<T> ok(T data, long total) { |
||||
return restData(data, SUCCESS, null, total); |
||||
} |
||||
|
||||
public static <T> R<T> error() { |
||||
return restResult(null, FAIL, "失败"); |
||||
} |
||||
|
||||
public static <T> R<T> error(String msg) { |
||||
return restResult(null, FAIL, msg); |
||||
} |
||||
|
||||
public static <T> R<T> error(T data) { |
||||
return restResult(data, FAIL, null); |
||||
} |
||||
|
||||
public static <T> R<T> error(T data, String msg) { |
||||
return restResult(data, FAIL, msg); |
||||
} |
||||
|
||||
public static <T> R<T> unAuth(String msg) { |
||||
return restResult(null, UNAUTH, msg); |
||||
} |
||||
|
||||
|
||||
private static <T> R<T> restResult(T data, int code, String msg) { |
||||
R<T> apiResult = new R<>(); |
||||
apiResult.setCode(code); |
||||
apiResult.setData(data); |
||||
apiResult.setMsg(msg); |
||||
return apiResult; |
||||
} |
||||
|
||||
private static <T> R<T> restData(T data, int code, String msg, long total) { |
||||
R<T> apiData = new R<>(); |
||||
apiData.setCode(code); |
||||
apiData.setMsg(msg); |
||||
apiData.setTotal(total); |
||||
apiData.setData(data); |
||||
return apiData; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
package com.cloud.kicc.common.core.config; |
||||
|
||||
import lombok.Data; |
||||
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
||||
import org.springframework.cloud.context.config.annotation.RefreshScope; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
*<p> |
||||
* 网关配置文件 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@Data |
||||
@RefreshScope |
||||
@ConfigurationProperties("gateway") |
||||
public class GatewayConfigProperties { |
||||
|
||||
/** |
||||
* 网关解密登录前端密码 |
||||
*/ |
||||
private String encodeKey; |
||||
|
||||
/** |
||||
* 网关忽略不需要校验验证码是否合法的客户端 |
||||
*/ |
||||
private List<String> ignoreClients; |
||||
|
||||
} |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
package com.cloud.kicc.common.core.config; |
||||
|
||||
import cn.hutool.core.date.DatePattern; |
||||
import com.cloud.kicc.common.core.jackson.KiccJavaTimeModule; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; |
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
|
||||
import java.time.ZoneId; |
||||
import java.util.Locale; |
||||
import java.util.TimeZone; |
||||
|
||||
/** |
||||
*<p> |
||||
* 配置全局JacksonConfig,影响mvc层的对象传输日期格式 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@Configuration(proxyBeanMethods = false) |
||||
@ConditionalOnClass(ObjectMapper.class) |
||||
@AutoConfigureBefore(org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.class) |
||||
public class JacksonAutoConfiguration { |
||||
|
||||
@Bean |
||||
@ConditionalOnMissingBean |
||||
public Jackson2ObjectMapperBuilderCustomizer customizer() { |
||||
return builder -> { |
||||
builder.locale(Locale.CHINA); |
||||
builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); |
||||
// 针对于Date类型,文本格式化,已经实现前端返回时间戳
|
||||
builder.simpleDateFormat(DatePattern.NORM_DATETIME_PATTERN); |
||||
// 解决返回给前端的Long类型数据失去精度,将Long转换为String
|
||||
builder.serializerByType(Long.class, ToStringSerializer.instance); |
||||
// 针对于JDK新时间类,序列化时带有T的问题,自定义格式化字符串
|
||||
builder.modules(new KiccJavaTimeModule()); |
||||
}; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
package com.cloud.kicc.common.core.config; |
||||
|
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.web.client.RestTemplate; |
||||
|
||||
/** |
||||
*<p> |
||||
* Rest 配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@Configuration(proxyBeanMethods = false) |
||||
public class RestTemplateConfiguration { |
||||
|
||||
@Bean |
||||
public RestTemplate restTemplate() { |
||||
return new RestTemplate(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
package com.cloud.kicc.common.core.config; |
||||
|
||||
import cn.hutool.core.date.DatePattern; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.format.FormatterRegistry; |
||||
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; |
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; |
||||
|
||||
import java.time.format.DateTimeFormatter; |
||||
|
||||
import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET; |
||||
|
||||
/** |
||||
*<p> |
||||
* 全局DateTimeFormat |
||||
* 针对GET请求传入参数转换 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@Configuration(proxyBeanMethods = false) |
||||
@ConditionalOnWebApplication(type = SERVLET) |
||||
public class WebMvcConfiguration implements WebMvcConfigurer { |
||||
|
||||
/** |
||||
* 增加GET请求参数中时间类型转换 {@link com.cloud.kicc.common.core.jackson.KiccJavaTimeModule} |
||||
* <ul> |
||||
* <li>HH:mm:ss -> LocalTime</li> |
||||
* <li>yyyy-MM-dd -> LocalDate</li> |
||||
* <li>yyyy-MM-dd HH:mm:ss -> LocalDateTime</li> |
||||
* </ul> |
||||
* @param registry |
||||
*/ |
||||
@Override |
||||
public void addFormatters(FormatterRegistry registry) { |
||||
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); |
||||
registrar.setTimeFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN)); |
||||
registrar.setDateFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)); |
||||
registrar.setDateTimeFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)); |
||||
registrar.registerFormatters(registry); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
package com.cloud.kicc.common.core.constant; |
||||
|
||||
/** |
||||
*<p> |
||||
* 应用前缀 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/24 |
||||
*/ |
||||
public interface AppConstants { |
||||
|
||||
String APP_SYSTEM = "/system"; |
||||
|
||||
String APP_MONITOR = "/monitor"; |
||||
|
||||
String APP_COMMON = "/common"; |
||||
|
||||
String APP_WORKFLOW = "/workflow"; |
||||
|
||||
String APP_REPORT = "/report"; |
||||
|
||||
String APP_BIGSCREEN = "/bigscreen"; |
||||
|
||||
} |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
package com.cloud.kicc.common.core.constant; |
||||
|
||||
/** |
||||
*<p> |
||||
* 缓存的key 常量 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
public interface CacheConstants { |
||||
|
||||
/** |
||||
* oauth 缓存前缀 |
||||
*/ |
||||
String OAUTH_ACCESS = ServiceNameConstants.AUTH_SERVICE + ":access:"; |
||||
|
||||
/** |
||||
* oauth 缓存令牌前缀 |
||||
*/ |
||||
String OAUTH_TOKEN = ServiceNameConstants.AUTH_SERVICE + ":token:"; |
||||
|
||||
/** |
||||
* 验证码前缀 |
||||
*/ |
||||
String VERIFICATION_CODE = ServiceNameConstants.SYSTEM_SERVICE + ":verification_code:"; |
||||
|
||||
/** |
||||
* oauth 客户端信息缓存 |
||||
*/ |
||||
String OAUTH_CLIENT_DETAILS = ServiceNameConstants.AUTH_SERVICE + ":client_details"; |
||||
|
||||
/** |
||||
* 菜单信息缓存 |
||||
*/ |
||||
String MENU_DETAILS = ServiceNameConstants.AUTH_SERVICE + ":menu_details"; |
||||
|
||||
/** |
||||
* 用户信息缓存 |
||||
*/ |
||||
String USER_DETAILS = ServiceNameConstants.AUTH_SERVICE + ":user_details"; |
||||
|
||||
/** |
||||
* 字典信息缓存 |
||||
*/ |
||||
String DICT_DETAILS = ServiceNameConstants.AUTH_SERVICE + ":dict_details"; |
||||
|
||||
/** |
||||
* 全局配置缓存 |
||||
*/ |
||||
String CONFIG_PARAM = ServiceNameConstants.AUTH_SERVICE + ":config_param"; |
||||
|
||||
} |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
package com.cloud.kicc.common.core.constant; |
||||
|
||||
/** |
||||
*<p> |
||||
* 通用常量 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
public interface CommonConstants { |
||||
|
||||
/** |
||||
* 正常 |
||||
*/ |
||||
String STATUS_NORMAL = "0"; |
||||
|
||||
/** |
||||
* 锁定 |
||||
*/ |
||||
String STATUS_LOCK = "9"; |
||||
|
||||
/** |
||||
* 编码 |
||||
*/ |
||||
String UTF8 = "UTF-8"; |
||||
|
||||
/** |
||||
* JSON 资源 |
||||
*/ |
||||
String CONTENT_TYPE = "application/json; charset=utf-8"; |
||||
|
||||
/** |
||||
* 前端工程名 |
||||
*/ |
||||
String FRONT_END_PROJECT = "kicc-ui"; |
||||
|
||||
/** |
||||
* 后端工程名 |
||||
*/ |
||||
String BACK_END_PROJECT = "kicc"; |
||||
|
||||
/** |
||||
* 当前页 |
||||
*/ |
||||
String CURRENT = "current"; |
||||
|
||||
/** |
||||
* size |
||||
*/ |
||||
String SIZE = "size"; |
||||
|
||||
} |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
package com.cloud.kicc.common.core.constant; |
||||
|
||||
/** |
||||
*<p> |
||||
* 正则表达式常量 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/4/18 |
||||
*/ |
||||
public interface RegexConstants { |
||||
|
||||
/** |
||||
* 匹配网址正则表达式 |
||||
*/ |
||||
String MATCHER_URL = "(((^https?:(?:\\/\\/)?)(?:[-;:&=\\+\\$,\\w]+@)?[A-Za-z0-9.-]+(?::\\d+)?|(?:www.|[-;:&=\\+\\$,\\w]+@)[A-Za-z0-9.-]+)((?:\\/[\\+~%\\/.\\w-_]*)?\\??(?:[-\\+=&;%@.\\w_{\\s\\S}]*)#?(?:[\\w]*))?)$"; |
||||
|
||||
} |
@ -0,0 +1,125 @@
@@ -0,0 +1,125 @@
|
||||
package com.cloud.kicc.common.core.constant; |
||||
|
||||
/** |
||||
*<p> |
||||
* 安全常量 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
public interface SecurityConstants { |
||||
|
||||
/** |
||||
* 角色前缀 |
||||
*/ |
||||
String ROLE = "ROLE_"; |
||||
|
||||
/** |
||||
* 项目的license |
||||
*/ |
||||
String PROJECT_LICENSE = "长沙康来生物有限公司"; |
||||
|
||||
/** |
||||
* 内部接口调用密钥 |
||||
*/ |
||||
String FROM_IN = "kG8qA6qG1aP5aR3g"; |
||||
|
||||
/** |
||||
* 内部接口调用Key标志 |
||||
*/ |
||||
String FROM = "from"; |
||||
|
||||
/** |
||||
* 请求header |
||||
*/ |
||||
String HEADER_FROM_IN = FROM + "=" + FROM_IN; |
||||
|
||||
/** |
||||
* 默认登录URL |
||||
*/ |
||||
String OAUTH_TOKEN_URL = "/oauth/token"; |
||||
|
||||
/** |
||||
* grant_type |
||||
*/ |
||||
String REFRESH_TOKEN = "refresh_token"; |
||||
|
||||
/** |
||||
* 手机号登录 |
||||
*/ |
||||
String APP = "app"; |
||||
|
||||
/** |
||||
* {bcrypt} 加密的特征码 |
||||
*/ |
||||
String BCRYPT = "{bcrypt}"; |
||||
|
||||
/** |
||||
* sys_oauth_client_details 表的字段,不包括client_id、client_secret |
||||
*/ |
||||
String CLIENT_FIELDS = "client_id, CONCAT('{noop}',client_secret) as client_secret, resource_ids, scope, " |
||||
+ "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, " |
||||
+ "refresh_token_validity, additional_information, autoapprove"; |
||||
|
||||
/** |
||||
* JdbcClientDetailsService 查询语句 |
||||
*/ |
||||
String BASE_FIND_STATEMENT = "select " + CLIENT_FIELDS + " from sys_oauth_client_details"; |
||||
|
||||
/** |
||||
* 默认的查询语句 |
||||
*/ |
||||
String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id"; |
||||
|
||||
/** |
||||
* 按条件client_id 查询 |
||||
*/ |
||||
String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?"; |
||||
|
||||
/*** |
||||
* 资源服务器默认bean名称 |
||||
*/ |
||||
String RESOURCE_SERVER_CONFIGURER = "resourceServerConfigurerAdapter"; |
||||
|
||||
/** |
||||
* 用户信息 |
||||
*/ |
||||
String DETAILS_USER = "user_info"; |
||||
|
||||
/** |
||||
* 协议字段 |
||||
*/ |
||||
String DETAILS_LICENSE = "license"; |
||||
|
||||
/** |
||||
* 验证码有效期,默认 60秒 |
||||
*/ |
||||
long CODE_TIME = 60; |
||||
|
||||
/** |
||||
* 手机验证码长度 |
||||
*/ |
||||
String PHONE_CODE_SIZE = "6"; |
||||
|
||||
/** |
||||
* 客户端模式 |
||||
*/ |
||||
String CLIENT_CREDENTIALS = "client_credentials"; |
||||
|
||||
/** |
||||
* 客户端ID |
||||
*/ |
||||
String CLIENT_ID = "clientId"; |
||||
|
||||
/** |
||||
* 模拟测试账户 |
||||
*/ |
||||
String MOCK_USERNAME = "admin"; |
||||
|
||||
/** |
||||
* 模拟测试密码 |
||||
*/ |
||||
String MOCK_PASSWORD = "kanglai123"; |
||||
|
||||
} |
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
package com.cloud.kicc.common.core.constant; |
||||
|
||||
/** |
||||
*<p> |
||||
* 服务名称 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
public interface ServiceNameConstants { |
||||
|
||||
/** |
||||
* 认证服务的SERVICE_ID |
||||
*/ |
||||
String AUTH_SERVICE = "kicc-auth"; |
||||
|
||||
/** |
||||
* SYSTEM模块 |
||||
*/ |
||||
String SYSTEM_SERVICE = "kicc-system-biz"; |
||||
|
||||
/** |
||||
* MONITOR模块 |
||||
*/ |
||||
String MONITOR_SERVICE = "kicc-monitor-biz"; |
||||
|
||||
/** |
||||
* COMMON模块 |
||||
*/ |
||||
String COMMON_SERVICE = "kicc-common-biz"; |
||||
|
||||
/** |
||||
* seata分布式事务演示-订单模块 |
||||
*/ |
||||
String SEATA_ORDER_SERVICE = "kicc-seata-order"; |
||||
|
||||
/** |
||||
* seata分布式事务演示-积分模块 |
||||
*/ |
||||
String SEATA_POINT_SERVICE = "kicc-seata-point"; |
||||
|
||||
|
||||
} |
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
package com.cloud.kicc.common.core.constant; |
||||
|
||||
/** |
||||
* Copy to kicc.common.util |
||||
* <p> |
||||
* Pool of <code>String</code> constants to prevent repeating of |
||||
* hard-coded <code>String</code> literals in the code. |
||||
* Due to fact that these are <code>public static final</code> |
||||
* they will be inlined by java compiler and |
||||
* reference to this class will be dropped. |
||||
* There is <b>no</b> performance gain of using this pool. |
||||
* Read: https://java.sun.com/docs/books/jls/third_edition/html/lexical.html#3.10.5
|
||||
* <ul> |
||||
* <li>Literal strings within the same class in the same package represent references to the same <code>String</code> object.</li> |
||||
* <li>Literal strings within different classes in the same package represent references to the same <code>String</code> object.</li> |
||||
* <li>Literal strings within different classes in different packages likewise represent references to the same <code>String</code> object.</li> |
||||
* <li>Strings computed by constant expressions are computed at compile time and then treated as if they were literals.</li> |
||||
* <li>Strings computed by concatenation at run time are newly created and therefore distinct.</li> |
||||
* </ul> |
||||
* @author wangxiang4 |
||||
*/ |
||||
public interface StringPool { |
||||
|
||||
String AMPERSAND = "&"; |
||||
String AND = "and"; |
||||
String AT = "@"; |
||||
String ASTERISK = "*"; |
||||
String STAR = ASTERISK; |
||||
String BACK_SLASH = "\\"; |
||||
String COLON = ":"; |
||||
String COMMA = ","; |
||||
String DASH = "-"; |
||||
String DOLLAR = "$"; |
||||
String DOT = "."; |
||||
String DOTDOT = ".."; |
||||
String DOT_CLASS = ".class"; |
||||
String DOT_JAVA = ".java"; |
||||
String DOT_XML = ".xml"; |
||||
String EMPTY = ""; |
||||
String EQUALS = "="; |
||||
String FALSE = "false"; |
||||
String SLASH = "/"; |
||||
String HASH = "#"; |
||||
String HAT = "^"; |
||||
String LEFT_BRACE = "{"; |
||||
String LEFT_BRACKET = "("; |
||||
String LEFT_CHEV = "<"; |
||||
String DOT_NEWLINE = ",\n"; |
||||
String NEWLINE = "\n"; |
||||
String N = "n"; |
||||
String NO = "no"; |
||||
String NULL = "null"; |
||||
String OFF = "off"; |
||||
String ON = "on"; |
||||
String PERCENT = "%"; |
||||
String PIPE = "|"; |
||||
String PLUS = "+"; |
||||
String QUESTION_MARK = "?"; |
||||
String EXCLAMATION_MARK = "!"; |
||||
String QUOTE = "\""; |
||||
String RETURN = "\r"; |
||||
String TAB = "\t"; |
||||
String RIGHT_BRACE = "}"; |
||||
String RIGHT_BRACKET = ")"; |
||||
String RIGHT_CHEV = ">"; |
||||
String SEMICOLON = ";"; |
||||
String SINGLE_QUOTE = "'"; |
||||
String BACKTICK = "`"; |
||||
String SPACE = " "; |
||||
String TILDA = "~"; |
||||
String LEFT_SQ_BRACKET = "["; |
||||
String RIGHT_SQ_BRACKET = "]"; |
||||
String TRUE = "true"; |
||||
String UNDERSCORE = "_"; |
||||
String UTF_8 = "UTF-8"; |
||||
String US_ASCII = "US-ASCII"; |
||||
String ISO_8859_1 = "ISO-8859-1"; |
||||
String Y = "y"; |
||||
String YES = "yes"; |
||||
String ONE = "1"; |
||||
String ZERO = "0"; |
||||
String DOLLAR_LEFT_BRACE = "${"; |
||||
String HASH_LEFT_BRACE = "#{"; |
||||
String CRLF = "\r\n"; |
||||
|
||||
String HTML_NBSP = " "; |
||||
String HTML_AMP = "&"; |
||||
String HTML_QUOTE = """; |
||||
String HTML_LT = "<"; |
||||
String HTML_GT = ">"; |
||||
|
||||
// ---------------------------------------------------------------- array
|
||||
|
||||
String[] EMPTY_ARRAY = new String[0]; |
||||
|
||||
byte[] BYTES_NEW_LINE = StringPool.NEWLINE.getBytes(); |
||||
} |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
package com.cloud.kicc.common.core.enums; |
||||
|
||||
import lombok.Getter; |
||||
import lombok.RequiredArgsConstructor; |
||||
|
||||
/** |
||||
*<p> |
||||
* API错误页面响应状态枚举 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Since: 2023/8/16 |
||||
*/ |
||||
@Getter |
||||
@RequiredArgsConstructor |
||||
public enum ExceptionEnum { |
||||
|
||||
UNAUTHORIZED_ACCESS(401, "禁止访问"), |
||||
PAGE_NOT_ACCESS(403, "页面无法访问"), |
||||
PAGE_NOT_FOUND(404, "网页未找到"), |
||||
ERROR(500, "错误"), |
||||
NET_WORK_ERROR(10000, "前端Js错误"), |
||||
PAGE_NOT_DATA(10100, "无数据页面"); |
||||
|
||||
/** |
||||
* 状态 |
||||
*/ |
||||
private final int value; |
||||
|
||||
/** |
||||
* 描述 |
||||
*/ |
||||
private final String description; |
||||
|
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
package com.cloud.kicc.common.core.enums; |
||||
|
||||
import lombok.Getter; |
||||
import lombok.RequiredArgsConstructor; |
||||
|
||||
/** |
||||
*<p> |
||||
* 社交登录类型 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@Getter |
||||
@RequiredArgsConstructor |
||||
public enum LoginTypeEnum { |
||||
|
||||
/** |
||||
* 账号密码登录 |
||||
*/ |
||||
PWD("PWD", "账号密码登录"), |
||||
|
||||
/** |
||||
* 验证码登录 |
||||
*/ |
||||
SMS("SMS", "验证码登录"); |
||||
|
||||
/** |
||||
* 类型 |
||||
*/ |
||||
private final String type; |
||||
|
||||
/** |
||||
* 描述 |
||||
*/ |
||||
private final String description; |
||||
|
||||
} |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
package com.cloud.kicc.common.core.exception; |
||||
|
||||
import lombok.NoArgsConstructor; |
||||
|
||||
/** |
||||
*<p> |
||||
* 检查异常 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@NoArgsConstructor |
||||
public class CheckedException extends RuntimeException { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
public CheckedException(String message) { |
||||
super(message); |
||||
} |
||||
|
||||
public CheckedException(Throwable cause) { |
||||
super(cause); |
||||
} |
||||
|
||||
public CheckedException(String message, Throwable cause) { |
||||
super(message, cause); |
||||
} |
||||
|
||||
public CheckedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { |
||||
super(message, cause, enableSuppression, writableStackTrace); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
package com.cloud.kicc.common.core.exception; |
||||
|
||||
/** |
||||
*<p> |
||||
* 验证码异常 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
public class ValidateCodeException extends RuntimeException { |
||||
|
||||
private static final long serialVersionUID = -7285211528095468156L; |
||||
|
||||
public ValidateCodeException() { |
||||
} |
||||
|
||||
public ValidateCodeException(String msg) { |
||||
super(msg); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
package com.cloud.kicc.common.core.factory; |
||||
|
||||
import lombok.AllArgsConstructor; |
||||
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; |
||||
import org.springframework.core.env.PropertiesPropertySource; |
||||
import org.springframework.core.env.PropertySource; |
||||
import org.springframework.core.io.support.EncodedResource; |
||||
import org.springframework.core.io.support.PropertySourceFactory; |
||||
|
||||
import java.io.FileNotFoundException; |
||||
import java.io.IOException; |
||||
import java.util.Properties; |
||||
|
||||
/** |
||||
*<p> |
||||
* 加载yml格式的自定义配置文件 |
||||
* @link https://blog.csdn.net/zxl8899/article/details/106382719/
|
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/3/7 |
||||
*/ |
||||
@AllArgsConstructor |
||||
public class YamlPropertySourceFactory implements PropertySourceFactory { |
||||
|
||||
@Override |
||||
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException { |
||||
Properties propertiesFromYaml = loadYamlIntoProperties(resource); |
||||
String sourceName = name != null ? name : resource.getResource().getFilename(); |
||||
assert sourceName != null; |
||||
return new PropertiesPropertySource(sourceName, propertiesFromYaml); |
||||
} |
||||
|
||||
private Properties loadYamlIntoProperties(EncodedResource resource) throws FileNotFoundException { |
||||
try { |
||||
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); |
||||
factory.setResources(resource.getResource()); |
||||
factory.afterPropertiesSet(); |
||||
return factory.getObject(); |
||||
} catch (IllegalStateException e) { |
||||
Throwable cause = e.getCause(); |
||||
if (cause instanceof FileNotFoundException) { |
||||
throw (FileNotFoundException) e.getCause(); |
||||
} |
||||
throw e; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
package com.cloud.kicc.common.core.jackson; |
||||
|
||||
import cn.hutool.core.date.DatePattern; |
||||
import com.fasterxml.jackson.databind.module.SimpleModule; |
||||
import com.fasterxml.jackson.datatype.jsr310.PackageVersion; |
||||
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer; |
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; |
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; |
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; |
||||
import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer; |
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; |
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; |
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; |
||||
|
||||
import java.time.Instant; |
||||
import java.time.LocalDate; |
||||
import java.time.LocalDateTime; |
||||
import java.time.LocalTime; |
||||
import java.time.format.DateTimeFormatter; |
||||
|
||||
/** |
||||
*<p> |
||||
* java 8 时间默认序列化 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/3/10 |
||||
*/ |
||||
public class KiccJavaTimeModule extends SimpleModule { |
||||
|
||||
public KiccJavaTimeModule() { |
||||
super(PackageVersion.VERSION); |
||||
|
||||
// ======================= 时间序列化规则 ===============================
|
||||
// LocalDateTime序列化时间戳
|
||||
this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))); |
||||
// LocalDate序列化时间戳
|
||||
this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)); |
||||
// LocalTime序列化时间戳
|
||||
this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME)); |
||||
// Instant 类型序列化
|
||||
this.addSerializer(Instant.class, InstantSerializer.INSTANCE); |
||||
|
||||
// ======================= 时间反序列化规则 ==============================
|
||||
// yyyy-MM-dd HH:mm:ss
|
||||
this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))); |
||||
// yyyy-MM-dd
|
||||
this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)); |
||||
// HH:mm:ss
|
||||
this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME)); |
||||
// Instant 反序列化
|
||||
this.addDeserializer(Instant.class, InstantDeserializer.INSTANT); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
package com.cloud.kicc.common.core.util; |
||||
|
||||
import cn.hutool.core.lang.generator.SnowflakeGenerator; |
||||
import lombok.experimental.UtilityClass; |
||||
|
||||
/** |
||||
*<p> |
||||
* 基础工具类 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/3/21 |
||||
*/ |
||||
@UtilityClass |
||||
public class BaseUtil { |
||||
|
||||
/** |
||||
* 雪花算法生成全局ID |
||||
* @Param |
||||
* @return |
||||
*/ |
||||
public Long snowflakeId () { |
||||
SnowflakeGenerator snowflakeGenerator = new SnowflakeGenerator(); |
||||
return snowflakeGenerator.next(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
package com.cloud.kicc.common.core.util; |
||||
|
||||
import lombok.experimental.UtilityClass; |
||||
import org.springframework.core.BridgeMethodResolver; |
||||
import org.springframework.core.DefaultParameterNameDiscoverer; |
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.core.ParameterNameDiscoverer; |
||||
import org.springframework.core.annotation.AnnotatedElementUtils; |
||||
import org.springframework.core.annotation.SynthesizingMethodParameter; |
||||
import org.springframework.web.method.HandlerMethod; |
||||
|
||||
import java.lang.annotation.Annotation; |
||||
import java.lang.reflect.Constructor; |
||||
import java.lang.reflect.Method; |
||||
|
||||
/** |
||||
*<p> |
||||
* 扩展类工具类 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@UtilityClass |
||||
public class ClassUtil extends org.springframework.util.ClassUtils { |
||||
|
||||
private final ParameterNameDiscoverer PARAMETERNAMEDISCOVERER = new DefaultParameterNameDiscoverer(); |
||||
|
||||
/** |
||||
* 获取方法参数信息 |
||||
* @param constructor 构造器 |
||||
* @param parameterIndex 参数序号 |
||||
* @return {MethodParameter} |
||||
*/ |
||||
public MethodParameter getMethodParameter(Constructor<?> constructor, int parameterIndex) { |
||||
MethodParameter methodParameter = new SynthesizingMethodParameter(constructor, parameterIndex); |
||||
methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER); |
||||
return methodParameter; |
||||
} |
||||
|
||||
/** |
||||
* 获取方法参数信息 |
||||
* @param method 方法 |
||||
* @param parameterIndex 参数序号 |
||||
* @return {MethodParameter} |
||||
*/ |
||||
public MethodParameter getMethodParameter(Method method, int parameterIndex) { |
||||
MethodParameter methodParameter = new SynthesizingMethodParameter(method, parameterIndex); |
||||
methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER); |
||||
return methodParameter; |
||||
} |
||||
|
||||
/** |
||||
* 获取Annotation |
||||
* @param method Method |
||||
* @param annotationType 注解类 |
||||
* @param <A> 泛型标记 |
||||
* @return {Annotation} |
||||
*/ |
||||
public <A extends Annotation> A getAnnotation(Method method, Class<A> annotationType) { |
||||
Class<?> targetClass = method.getDeclaringClass(); |
||||
// The method may be on an interface, but we need attributes from the target
|
||||
// class.
|
||||
// If the target class is null, the method will be unchanged.
|
||||
Method specificMethod = ClassUtil.getMostSpecificMethod(method, targetClass); |
||||
// If we are dealing with method with generic parameters, find the original
|
||||
// method.
|
||||
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); |
||||
// 先找方法,再找方法上的类
|
||||
A annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod, annotationType); |
||||
; |
||||
if (null != annotation) { |
||||
return annotation; |
||||
} |
||||
// 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类
|
||||
return AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(), annotationType); |
||||
} |
||||
|
||||
/** |
||||
* 获取Annotation |
||||
* @param handlerMethod HandlerMethod |
||||
* @param annotationType 注解类 |
||||
* @param <A> 泛型标记 |
||||
* @return {Annotation} |
||||
*/ |
||||
public <A extends Annotation> A getAnnotation(HandlerMethod handlerMethod, Class<A> annotationType) { |
||||
// 先找方法,再找方法上的类
|
||||
A annotation = handlerMethod.getMethodAnnotation(annotationType); |
||||
if (null != annotation) { |
||||
return annotation; |
||||
} |
||||
// 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类
|
||||
Class<?> beanType = handlerMethod.getBeanType(); |
||||
return AnnotatedElementUtils.findMergedAnnotation(beanType, annotationType); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
package com.cloud.kicc.common.core.util; |
||||
|
||||
import java.lang.management.ManagementFactory; |
||||
import java.text.SimpleDateFormat; |
||||
import java.util.Date; |
||||
import java.util.TimeZone; |
||||
|
||||
/** |
||||
*<p> |
||||
* 时间 util |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @since: 2023/6/13 |
||||
*/ |
||||
public class DateUtil extends cn.hutool.core.date.DateUtil { |
||||
|
||||
/** |
||||
* 获取服务器启动时间 |
||||
*/ |
||||
public static Date getServerStartDate() { |
||||
long time = ManagementFactory.getRuntimeMXBean().getStartTime(); |
||||
return new Date(time); |
||||
} |
||||
|
||||
public static String formatDate(long dateTime, String pattern) { |
||||
return format(new Date(dateTime), pattern); |
||||
} |
||||
|
||||
/** |
||||
* 日期型字符串转化为日期 格式 |
||||
*/ |
||||
public static Date parseDate(Object str) { |
||||
if (str == null) { |
||||
return null; |
||||
} |
||||
return parse(str.toString()); |
||||
} |
||||
|
||||
public static final String parseDateToStr(final String format, final Date date) { |
||||
return new SimpleDateFormat(format).format(date); |
||||
} |
||||
|
||||
public static final String formatUTC(final Date date, final String format) { |
||||
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); |
||||
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); |
||||
return simpleDateFormat.format(date); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,113 @@
@@ -0,0 +1,113 @@
|
||||
package com.cloud.kicc.common.core.util; |
||||
|
||||
import cn.hutool.core.io.IoUtil; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import java.io.*; |
||||
import java.net.URLEncoder; |
||||
import java.text.DecimalFormat; |
||||
|
||||
/** |
||||
* @author yong |
||||
* @date 2020/3/22 |
||||
* @description 文件工具类 |
||||
*/ |
||||
public class FileUtil extends cn.hutool.core.io.FileUtil { |
||||
public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; |
||||
|
||||
/** |
||||
* 转换文件大小 |
||||
* |
||||
* @param fileS |
||||
* @return |
||||
*/ |
||||
public static String fileSize(long fileS) { |
||||
DecimalFormat df = new DecimalFormat("#.00"); |
||||
String fileSizeString = ""; |
||||
String wrongSize = "0B"; |
||||
if (fileS == 0) { |
||||
return wrongSize; |
||||
} |
||||
if (fileS < 1024) { |
||||
fileSizeString = df.format((double) fileS) + "B"; |
||||
} else if (fileS < 1048576) { |
||||
fileSizeString = df.format((double) fileS / 1024) + "KB"; |
||||
} else if (fileS < 1073741824) { |
||||
fileSizeString = df.format((double) fileS / 1048576) + "MB"; |
||||
} else { |
||||
fileSizeString = df.format((double) fileS / 1073741824) + "GB"; |
||||
} |
||||
return fileSizeString; |
||||
} |
||||
|
||||
public static void copyInputStreamToFile(InputStream source, File destination) throws IOException { |
||||
try { |
||||
copyToFile(source, destination); |
||||
} finally { |
||||
IoUtil.close(source); |
||||
} |
||||
|
||||
} |
||||
|
||||
public static void copyToFile(InputStream source, File destination) throws IOException { |
||||
FileOutputStream output = openOutputStream(destination); |
||||
|
||||
try { |
||||
IoUtil.copy(source, output); |
||||
output.close(); |
||||
} finally { |
||||
IoUtil.close(output); |
||||
} |
||||
|
||||
} |
||||
|
||||
public static FileOutputStream openOutputStream(File file) throws IOException { |
||||
return openOutputStream(file, false); |
||||
} |
||||
|
||||
public static FileOutputStream openOutputStream(File file, boolean append) throws IOException { |
||||
if (file.exists()) { |
||||
if (file.isDirectory()) { |
||||
throw new IOException("File '" + file + "' exists but is a directory"); |
||||
} |
||||
|
||||
if (!file.canWrite()) { |
||||
throw new IOException("File '" + file + "' cannot be written to"); |
||||
} |
||||
} else { |
||||
File parent = file.getParentFile(); |
||||
if (parent != null && !parent.mkdirs() && !parent.isDirectory()) { |
||||
throw new IOException("Directory '" + parent + "' could not be created"); |
||||
} |
||||
} |
||||
|
||||
return new FileOutputStream(file, append); |
||||
} |
||||
/** |
||||
* 下载文件名重新编码 |
||||
* |
||||
* @param request 请求对象 |
||||
* @param fileName 文件名 |
||||
* @return 编码后的文件名 |
||||
*/ |
||||
public static String setFileDownloadHeader(HttpServletRequest request, String fileName) |
||||
throws UnsupportedEncodingException { |
||||
final String agent = request.getHeader("USER-AGENT"); |
||||
String filename = fileName; |
||||
if (agent.contains("MSIE")) { |
||||
// IE浏览器
|
||||
filename = URLEncoder.encode(filename, "utf-8"); |
||||
filename = filename.replace("+", " "); |
||||
} else if (agent.contains("Firefox")) { |
||||
// 火狐浏览器
|
||||
filename = new String(fileName.getBytes(), "ISO8859-1"); |
||||
} else if (agent.contains("Chrome")) { |
||||
// google浏览器
|
||||
filename = URLEncoder.encode(filename, "utf-8"); |
||||
} else { |
||||
// 其它浏览器
|
||||
filename = URLEncoder.encode(filename, "utf-8"); |
||||
} |
||||
return filename; |
||||
} |
||||
} |
@ -0,0 +1,499 @@
@@ -0,0 +1,499 @@
|
||||
package com.cloud.kicc.common.core.util; |
||||
|
||||
import java.util.*; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
import java.util.concurrent.ConcurrentMap; |
||||
import java.util.regex.Matcher; |
||||
import java.util.regex.Pattern; |
||||
|
||||
/** |
||||
*<p> |
||||
* HTML过滤器,用于去除XSS漏洞隐患。 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/19 |
||||
*/ |
||||
public final class HTMLFilterUtil { |
||||
/** |
||||
* regex flag union representing /si modifiers in php |
||||
**/ |
||||
private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; |
||||
private static final Pattern P_COMMENTS = Pattern.compile("<!--(.*?)-->", Pattern.DOTALL); |
||||
private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); |
||||
private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); |
||||
private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); |
||||
private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); |
||||
private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); |
||||
private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); |
||||
private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); |
||||
private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); |
||||
private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); |
||||
private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); |
||||
private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); |
||||
private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); |
||||
private static final Pattern P_END_ARROW = Pattern.compile("^>"); |
||||
private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); |
||||
private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); |
||||
private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); |
||||
private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); |
||||
private static final Pattern P_AMP = Pattern.compile("&"); |
||||
private static final Pattern P_QUOTE = Pattern.compile("\""); |
||||
private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); |
||||
private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); |
||||
private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); |
||||
|
||||
// @xxx could grow large... maybe use sesat's ReferenceMap
|
||||
private static final ConcurrentMap<String, Pattern> P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); |
||||
private static final ConcurrentMap<String, Pattern> P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); |
||||
|
||||
/** |
||||
* set of allowed html elements, along with allowed attributes for each element |
||||
**/ |
||||
private final Map<String, List<String>> vAllowed; |
||||
/** |
||||
* counts of open tags for each (allowable) html element |
||||
**/ |
||||
private final Map<String, Integer> vTagCounts = new HashMap<>(); |
||||
|
||||
/** |
||||
* html elements which must always be self-closing (e.g. "<img />") |
||||
**/ |
||||
private final String[] vSelfClosingTags; |
||||
/** |
||||
* html elements which must always have separate opening and closing tags (e.g. "<b></b>") |
||||
**/ |
||||
private final String[] vNeedClosingTags; |
||||
/** |
||||
* set of disallowed html elements |
||||
**/ |
||||
private final String[] vDisallowed; |
||||
/** |
||||
* attributes which should be checked for valid protocols |
||||
**/ |
||||
private final String[] vProtocolAtts; |
||||
/** |
||||
* allowed protocols |
||||
**/ |
||||
private final String[] vAllowedProtocols; |
||||
/** |
||||
* tags which should be removed if they contain no content (e.g. "<b></b>" or "<b />") |
||||
**/ |
||||
private final String[] vRemoveBlanks; |
||||
/** |
||||
* entities allowed within html markup |
||||
**/ |
||||
private final String[] vAllowedEntities; |
||||
/** |
||||
* flag determining whether comments are allowed in input String. |
||||
*/ |
||||
private final boolean stripComment; |
||||
private final boolean encodeQuotes; |
||||
/** |
||||
* flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "<b text </b>" |
||||
* becomes "<b> text </b>"). If set to false, unbalanced angle brackets will be html escaped. |
||||
*/ |
||||
private final boolean alwaysMakeTags; |
||||
|
||||
/** |
||||
* Default constructor. |
||||
*/ |
||||
public HTMLFilterUtil() { |
||||
vAllowed = new HashMap<>(); |
||||
|
||||
final ArrayList<String> a_atts = new ArrayList<>(); |
||||
a_atts.add("href"); |
||||
a_atts.add("target"); |
||||
vAllowed.put("a", a_atts); |
||||
|
||||
final ArrayList<String> img_atts = new ArrayList<>(); |
||||
img_atts.add("src"); |
||||
img_atts.add("width"); |
||||
img_atts.add("height"); |
||||
img_atts.add("alt"); |
||||
vAllowed.put("img", img_atts); |
||||
|
||||
final ArrayList<String> no_atts = new ArrayList<>(); |
||||
vAllowed.put("b", no_atts); |
||||
vAllowed.put("strong", no_atts); |
||||
vAllowed.put("i", no_atts); |
||||
vAllowed.put("em", no_atts); |
||||
|
||||
vSelfClosingTags = new String[]{"img"}; |
||||
vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"}; |
||||
vDisallowed = new String[]{}; |
||||
vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp.
|
||||
vProtocolAtts = new String[]{"src", "href"}; |
||||
vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"}; |
||||
vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"}; |
||||
stripComment = true; |
||||
encodeQuotes = true; |
||||
alwaysMakeTags = true; |
||||
} |
||||
|
||||
/** |
||||
* Map-parameter configurable constructor. |
||||
* |
||||
* @param conf map containing configuration. keys match field names. |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public HTMLFilterUtil(final Map<String, Object> conf) { |
||||
|
||||
assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; |
||||
assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; |
||||
assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; |
||||
assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; |
||||
assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; |
||||
assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; |
||||
assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; |
||||
assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; |
||||
|
||||
vAllowed = Collections.unmodifiableMap((HashMap<String, List<String>>) conf.get("vAllowed")); |
||||
vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); |
||||
vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); |
||||
vDisallowed = (String[]) conf.get("vDisallowed"); |
||||
vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); |
||||
vProtocolAtts = (String[]) conf.get("vProtocolAtts"); |
||||
vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); |
||||
vAllowedEntities = (String[]) conf.get("vAllowedEntities"); |
||||
stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; |
||||
encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; |
||||
alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; |
||||
} |
||||
|
||||
private void reset() { |
||||
vTagCounts.clear(); |
||||
} |
||||
|
||||
// ---------------------------------------------------------------
|
||||
// my versions of some PHP library functions
|
||||
public static String chr(final int decimal) { |
||||
return String.valueOf((char) decimal); |
||||
} |
||||
|
||||
public static String htmlSpecialChars(final String s) { |
||||
String result = s; |
||||
result = regexReplace(P_AMP, "&", result); |
||||
result = regexReplace(P_QUOTE, """, result); |
||||
result = regexReplace(P_LEFT_ARROW, "<", result); |
||||
result = regexReplace(P_RIGHT_ARROW, ">", result); |
||||
return result; |
||||
} |
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/** |
||||
* given a user submitted input String, filter out any invalid or restricted html. |
||||
* |
||||
* @param input text (i.e. submitted by a user) than may contain html |
||||
* @return "clean" version of input, with only valid, whitelisted html elements allowed |
||||
*/ |
||||
public String filter(final String input) { |
||||
reset(); |
||||
String s = input; |
||||
|
||||
s = escapeComments(s); |
||||
|
||||
s = balanceHTML(s); |
||||
|
||||
s = checkTags(s); |
||||
|
||||
s = processRemoveBlanks(s); |
||||
|
||||
s = validateEntities(s); |
||||
|
||||
return s; |
||||
} |
||||
|
||||
public boolean isAlwaysMakeTags() { |
||||
return alwaysMakeTags; |
||||
} |
||||
|
||||
public boolean isStripComments() { |
||||
return stripComment; |
||||
} |
||||
|
||||
private String escapeComments(final String s) { |
||||
final Matcher m = P_COMMENTS.matcher(s); |
||||
final StringBuffer buf = new StringBuffer(); |
||||
if (m.find()) { |
||||
final String match = m.group(1); // (.*?)
|
||||
m.appendReplacement(buf, Matcher.quoteReplacement("<!--" + htmlSpecialChars(match) + "-->")); |
||||
} |
||||
m.appendTail(buf); |
||||
|
||||
return buf.toString(); |
||||
} |
||||
|
||||
private String balanceHTML(String s) { |
||||
if (alwaysMakeTags) { |
||||
//
|
||||
// try and form html
|
||||
//
|
||||
s = regexReplace(P_END_ARROW, "", s); |
||||
s = regexReplace(P_BODY_TO_END, "<$1>", s); |
||||
s = regexReplace(P_XML_CONTENT, "$1<$2", s); |
||||
|
||||
} else { |
||||
//
|
||||
// escape stray brackets
|
||||
//
|
||||
s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); |
||||
s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); |
||||
|
||||
//
|
||||
// the last regexp causes '<>' entities to appear
|
||||
// (we need to do a lookahead assertion so that the last bracket can
|
||||
// be used in the next pass of the regexp)
|
||||
//
|
||||
s = regexReplace(P_BOTH_ARROWS, "", s); |
||||
} |
||||
|
||||
return s; |
||||
} |
||||
|
||||
private String checkTags(String s) { |
||||
Matcher m = P_TAGS.matcher(s); |
||||
|
||||
final StringBuffer buf = new StringBuffer(); |
||||
while (m.find()) { |
||||
String replaceStr = m.group(1); |
||||
replaceStr = processTag(replaceStr); |
||||
m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); |
||||
} |
||||
m.appendTail(buf); |
||||
|
||||
// these get tallied in processTag
|
||||
// (remember to reset before subsequent calls to filter method)
|
||||
final StringBuilder sBuilder = new StringBuilder(buf.toString()); |
||||
for (String key : vTagCounts.keySet()) { |
||||
for (int ii = 0; ii < vTagCounts.get(key); ii++) { |
||||
sBuilder.append("</").append(key).append(">"); |
||||
} |
||||
} |
||||
s = sBuilder.toString(); |
||||
|
||||
return s; |
||||
} |
||||
|
||||
private String processRemoveBlanks(final String s) { |
||||
String result = s; |
||||
for (String tag : vRemoveBlanks) { |
||||
if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) { |
||||
P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?></" + tag + ">")); |
||||
} |
||||
result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); |
||||
if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) { |
||||
P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); |
||||
} |
||||
result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) { |
||||
Matcher m = regex_pattern.matcher(s); |
||||
return m.replaceAll(replacement); |
||||
} |
||||
|
||||
private String processTag(final String s) { |
||||
// ending tags
|
||||
Matcher m = P_END_TAG.matcher(s); |
||||
if (m.find()) { |
||||
final String name = m.group(1).toLowerCase(); |
||||
if (allowed(name)) { |
||||
if (false == inArray(name, vSelfClosingTags)) { |
||||
if (vTagCounts.containsKey(name)) { |
||||
vTagCounts.put(name, vTagCounts.get(name) - 1); |
||||
return "</" + name + ">"; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// starting tags
|
||||
m = P_START_TAG.matcher(s); |
||||
if (m.find()) { |
||||
final String name = m.group(1).toLowerCase(); |
||||
final String body = m.group(2); |
||||
String ending = m.group(3); |
||||
|
||||
// debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" );
|
||||
if (allowed(name)) { |
||||
final StringBuilder params = new StringBuilder(); |
||||
|
||||
final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); |
||||
final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); |
||||
final List<String> paramNames = new ArrayList<>(); |
||||
final List<String> paramValues = new ArrayList<>(); |
||||
while (m2.find()) { |
||||
paramNames.add(m2.group(1)); // ([a-z0-9]+)
|
||||
paramValues.add(m2.group(3)); // (.*?)
|
||||
} |
||||
while (m3.find()) { |
||||
paramNames.add(m3.group(1)); // ([a-z0-9]+)
|
||||
paramValues.add(m3.group(3)); // ([^\"\\s']+)
|
||||
} |
||||
|
||||
String paramName, paramValue; |
||||
for (int ii = 0; ii < paramNames.size(); ii++) { |
||||
paramName = paramNames.get(ii).toLowerCase(); |
||||
paramValue = paramValues.get(ii); |
||||
|
||||
// debug( "paramName='" + paramName + "'" );
|
||||
// debug( "paramValue='" + paramValue + "'" );
|
||||
// debug( "allowed? " + vAllowed.get( name ).contains( paramName ) );
|
||||
|
||||
if (allowedAttribute(name, paramName)) { |
||||
if (inArray(paramName, vProtocolAtts)) { |
||||
paramValue = processParamProtocol(paramValue); |
||||
} |
||||
params.append(' ').append(paramName).append("=\"").append(paramValue).append("\""); |
||||
} |
||||
} |
||||
|
||||
if (inArray(name, vSelfClosingTags)) { |
||||
ending = " /"; |
||||
} |
||||
|
||||
if (inArray(name, vNeedClosingTags)) { |
||||
ending = ""; |
||||
} |
||||
|
||||
if (ending == null || ending.length() < 1) { |
||||
if (vTagCounts.containsKey(name)) { |
||||
vTagCounts.put(name, vTagCounts.get(name) + 1); |
||||
} else { |
||||
vTagCounts.put(name, 1); |
||||
} |
||||
} else { |
||||
ending = " /"; |
||||
} |
||||
return "<" + name + params + ending + ">"; |
||||
} else { |
||||
return ""; |
||||
} |
||||
} |
||||
|
||||
// comments
|
||||
m = P_COMMENT.matcher(s); |
||||
if (!stripComment && m.find()) { |
||||
return "<" + m.group() + ">"; |
||||
} |
||||
|
||||
return ""; |
||||
} |
||||
|
||||
private String processParamProtocol(String s) { |
||||
s = decodeEntities(s); |
||||
final Matcher m = P_PROTOCOL.matcher(s); |
||||
if (m.find()) { |
||||
final String protocol = m.group(1); |
||||
if (!inArray(protocol, vAllowedProtocols)) { |
||||
// bad protocol, turn into local anchor link instead
|
||||
s = "#" + s.substring(protocol.length() + 1); |
||||
if (s.startsWith("#//")) { |
||||
s = "#" + s.substring(3); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return s; |
||||
} |
||||
|
||||
private String decodeEntities(String s) { |
||||
StringBuffer buf = new StringBuffer(); |
||||
|
||||
Matcher m = P_ENTITY.matcher(s); |
||||
while (m.find()) { |
||||
final String match = m.group(1); |
||||
final int decimal = Integer.decode(match).intValue(); |
||||
m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); |
||||
} |
||||
m.appendTail(buf); |
||||
s = buf.toString(); |
||||
|
||||
buf = new StringBuffer(); |
||||
m = P_ENTITY_UNICODE.matcher(s); |
||||
while (m.find()) { |
||||
final String match = m.group(1); |
||||
final int decimal = Integer.valueOf(match, 16).intValue(); |
||||
m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); |
||||
} |
||||
m.appendTail(buf); |
||||
s = buf.toString(); |
||||
|
||||
buf = new StringBuffer(); |
||||
m = P_ENCODE.matcher(s); |
||||
while (m.find()) { |
||||
final String match = m.group(1); |
||||
final int decimal = Integer.valueOf(match, 16).intValue(); |
||||
m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); |
||||
} |
||||
m.appendTail(buf); |
||||
s = buf.toString(); |
||||
|
||||
s = validateEntities(s); |
||||
return s; |
||||
} |
||||
|
||||
private String validateEntities(final String s) { |
||||
StringBuffer buf = new StringBuffer(); |
||||
|
||||
// validate entities throughout the string
|
||||
Matcher m = P_VALID_ENTITIES.matcher(s); |
||||
while (m.find()) { |
||||
final String one = m.group(1); // ([^&;]*)
|
||||
final String two = m.group(2); // (?=(;|&|$))
|
||||
m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); |
||||
} |
||||
m.appendTail(buf); |
||||
|
||||
return encodeQuotes(buf.toString()); |
||||
} |
||||
|
||||
private String encodeQuotes(final String s) { |
||||
if (encodeQuotes) { |
||||
StringBuffer buf = new StringBuffer(); |
||||
Matcher m = P_VALID_QUOTES.matcher(s); |
||||
while (m.find()) { |
||||
final String one = m.group(1); // (>|^)
|
||||
final String two = m.group(2); // ([^<]+?)
|
||||
final String three = m.group(3); // (<|$)
|
||||
// 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two)
|
||||
m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three)); |
||||
} |
||||
m.appendTail(buf); |
||||
return buf.toString(); |
||||
} else { |
||||
return s; |
||||
} |
||||
} |
||||
|
||||
private String checkEntity(final String preamble, final String term) { |
||||
|
||||
return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; |
||||
} |
||||
|
||||
private boolean isValidEntity(final String entity) { |
||||
return inArray(entity, vAllowedEntities); |
||||
} |
||||
|
||||
private static boolean inArray(final String s, final String[] array) { |
||||
for (String item : array) { |
||||
if (item != null && item.equals(s)) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private boolean allowed(final String name) { |
||||
return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); |
||||
} |
||||
|
||||
private boolean allowedAttribute(final String name, final String paramName) { |
||||
return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); |
||||
} |
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
package com.cloud.kicc.common.core.util; |
||||
|
||||
import com.ulisesbocchio.jasyptspringboot.encryptor.DefaultLazyEncryptor; |
||||
import org.jasypt.encryption.StringEncryptor; |
||||
import org.springframework.core.env.StandardEnvironment; |
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
||||
import org.springframework.security.crypto.password.PasswordEncoder; |
||||
|
||||
/** |
||||
*<p> |
||||
* Jasypt加解密单元测试 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/24 |
||||
*/ |
||||
public class JasyptUtil { |
||||
|
||||
public static void main(String[] args) { |
||||
testEnvironmentProperties(); |
||||
} |
||||
|
||||
public static void testEnvironmentProperties() { |
||||
System.setProperty("jasypt.encryptor.password", "kicc"); |
||||
|
||||
PasswordEncoder ENCODER = new BCryptPasswordEncoder(); |
||||
|
||||
System.out.println(ENCODER.encode("123456")); |
||||
|
||||
StringEncryptor stringEncryptor = new DefaultLazyEncryptor(new StandardEnvironment()); |
||||
//加密方法
|
||||
System.out.println(stringEncryptor.encrypt("kicc")); |
||||
|
||||
//解密方法
|
||||
System.out.println(stringEncryptor.decrypt("6GBMom2U/XAHuMG3OSkOMw==")); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,203 @@
@@ -0,0 +1,203 @@
|
||||
package com.cloud.kicc.common.core.util; |
||||
|
||||
import cn.hutool.core.exceptions.UtilException; |
||||
import cn.hutool.core.text.StrBuilder; |
||||
import cn.hutool.core.util.CharsetUtil; |
||||
import cn.hutool.core.util.StrUtil; |
||||
|
||||
/** |
||||
*<p> |
||||
* 拼音工具类 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/24 |
||||
*/ |
||||
public class PinyinUtil { |
||||
/** 汉字对应ascii范围 */ |
||||
private static int[] pinyinValue = new int[] { -20319, -20317, -20304, -20295, -20292, -20283, -20265, -20257, -20242, -20230, -20051, -20036, -20032, -20026, -20002, -19990, -19986, -19982, |
||||
-19976, -19805, -19784, -19775, -19774, -19763, -19756, -19751, -19746, -19741, -19739, -19728, -19725, -19715, -19540, -19531, -19525, -19515, -19500, -19484, -19479, -19467, -19289, |
||||
-19288, -19281, -19275, -19270, -19263, -19261, -19249, -19243, -19242, -19238, -19235, -19227, -19224, -19218, -19212, -19038, -19023, -19018, -19006, -19003, -18996, -18977, -18961, |
||||
-18952, -18783, -18774, -18773, -18763, -18756, -18741, -18735, -18731, -18722, -18710, -18697, -18696, -18526, -18518, -18501, -18490, -18478, -18463, -18448, -18447, -18446, -18239, |
||||
-18237, -18231, -18220, -18211, -18201, -18184, -18183, -18181, -18012, -17997, -17988, -17970, -17964, -17961, -17950, -17947, -17931, -17928, -17922, -17759, -17752, -17733, -17730, |
||||
-17721, -17703, -17701, -17697, -17692, -17683, -17676, -17496, -17487, -17482, -17468, -17454, -17433, -17427, -17417, -17202, -17185, -16983, -16970, -16942, -16915, -16733, -16708, |
||||
-16706, -16689, -16664, -16657, -16647, -16474, -16470, -16465, -16459, -16452, -16448, -16433, -16429, -16427, -16423, -16419, -16412, -16407, -16403, -16401, -16393, -16220, -16216, |
||||
-16212, -16205, -16202, -16187, -16180, -16171, -16169, -16158, -16155, -15959, -15958, -15944, -15933, -15920, -15915, -15903, -15889, -15878, -15707, -15701, -15681, -15667, -15661, |
||||
-15659, -15652, -15640, -15631, -15625, -15454, -15448, -15436, -15435, -15419, -15416, -15408, -15394, -15385, -15377, -15375, -15369, -15363, -15362, -15183, -15180, -15165, -15158, |
||||
-15153, -15150, -15149, -15144, -15143, -15141, -15140, -15139, -15128, -15121, -15119, -15117, -15110, -15109, -14941, -14937, -14933, -14930, -14929, -14928, -14926, -14922, -14921, |
||||
-14914, -14908, -14902, -14894, -14889, -14882, -14873, -14871, -14857, -14678, -14674, -14670, -14668, -14663, -14654, -14645, -14630, -14594, -14429, -14407, -14399, -14384, -14379, |
||||
-14368, -14355, -14353, -14345, -14170, -14159, -14151, -14149, -14145, -14140, -14137, -14135, -14125, -14123, -14122, -14112, -14109, -14099, -14097, -14094, -14092, -14090, -14087, |
||||
-14083, -13917, -13914, -13910, -13907, -13906, -13905, -13896, -13894, -13878, -13870, -13859, -13847, -13831, -13658, -13611, -13601, -13406, -13404, -13400, -13398, -13395, -13391, |
||||
-13387, -13383, -13367, -13359, -13356, -13343, -13340, -13329, -13326, -13318, -13147, -13138, -13120, -13107, -13096, -13095, -13091, -13076, -13068, -13063, -13060, -12888, -12875, |
||||
-12871, -12860, -12858, -12852, -12849, -12838, -12831, -12829, -12812, -12802, -12607, -12597, -12594, -12585, -12556, -12359, -12346, -12320, -12300, -12120, -12099, -12089, -12074, |
||||
-12067, -12058, -12039, -11867, -11861, -11847, -11831, -11798, -11781, -11604, -11589, -11536, -11358, -11340, -11339, -11324, -11303, -11097, -11077, -11067, -11055, -11052, -11045, |
||||
-11041, -11038, -11024, -11020, -11019, -11018, -11014, -10838, -10832, -10815, -10800, -10790, -10780, -10764, -10587, -10544, -10533, -10519, -10331, -10329, -10328, -10322, -10315, |
||||
-10309, -10307, -10296, -10281, -10274, -10270, -10262, -10260, -10256, -10254 }; |
||||
|
||||
private static String[] pinyinStr = new String[] { "a", "ai", "an", "ang", "ao", "ba", "bai", "ban", "bang", "bao", "bei", "ben", "beng", "bi", "bian", "biao", "bie", "bin", "bing", "bo", "bu", |
||||
"ca", "cai", "can", "cang", "cao", "ce", "ceng", "cha", "chai", "chan", "chang", "chao", "che", "chen", "cheng", "chi", "chong", "chou", "chu", "chuai", "chuan", "chuang", "chui", "chun", |
||||
"chuo", "ci", "cong", "cou", "cu", "cuan", "cui", "cun", "cuo", "da", "dai", "dan", "dang", "dao", "de", "deng", "di", "dian", "diao", "die", "ding", "diu", "dong", "dou", "du", "duan", |
||||
"dui", "dun", "duo", "e", "en", "er", "fa", "fan", "fang", "fei", "fen", "feng", "fo", "fou", "fu", "ga", "gai", "gan", "gang", "gao", "ge", "gei", "gen", "geng", "gong", "gou", "gu", |
||||
"gua", "guai", "guan", "guang", "gui", "gun", "guo", "ha", "hai", "han", "hang", "hao", "he", "hei", "hen", "heng", "hong", "hou", "hu", "hua", "huai", "huan", "huang", "hui", "hun", |
||||
"huo", "ji", "jia", "jian", "jiang", "jiao", "jie", "jin", "jing", "jiong", "jiu", "ju", "juan", "jue", "jun", "ka", "kai", "kan", "kang", "kao", "ke", "ken", "keng", "kong", "kou", "ku", |
||||
"kua", "kuai", "kuan", "kuang", "kui", "kun", "kuo", "la", "lai", "lan", "lang", "lao", "le", "lei", "leng", "li", "lia", "lian", "liang", "liao", "lie", "lin", "ling", "liu", "long", |
||||
"lou", "lu", "lv", "luan", "lue", "lun", "luo", "ma", "mai", "man", "mang", "mao", "me", "mei", "men", "meng", "mi", "mian", "miao", "mie", "min", "ming", "miu", "mo", "mou", "mu", "na", |
||||
"nai", "nan", "nang", "nao", "ne", "nei", "nen", "neng", "ni", "nian", "niang", "niao", "nie", "nin", "ning", "niu", "nong", "nu", "nv", "nuan", "nue", "nuo", "o", "ou", "pa", "pai", |
||||
"pan", "pang", "pao", "pei", "pen", "peng", "pi", "pian", "piao", "pie", "pin", "ping", "po", "pu", "qi", "qia", "qian", "qiang", "qiao", "qie", "qin", "qing", "qiong", "qiu", "qu", |
||||
"quan", "que", "qun", "ran", "rang", "rao", "re", "ren", "reng", "ri", "rong", "rou", "ru", "ruan", "rui", "run", "ruo", "sa", "sai", "san", "sang", "sao", "se", "sen", "seng", "sha", |
||||
"shai", "shan", "shang", "shao", "she", "shen", "sheng", "shi", "shou", "shu", "shua", "shuai", "shuan", "shuang", "shui", "shun", "shuo", "si", "song", "sou", "su", "suan", "sui", "sun", |
||||
"suo", "ta", "tai", "tan", "tang", "tao", "te", "teng", "ti", "tian", "tiao", "tie", "ting", "tong", "tou", "tu", "tuan", "tui", "tun", "tuo", "wa", "wai", "wan", "wang", "wei", "wen", |
||||
"weng", "wo", "wu", "xi", "xia", "xian", "xiang", "xiao", "xie", "xin", "xing", "xiong", "xiu", "xu", "xuan", "xue", "xun", "ya", "yan", "yang", "yao", "ye", "yi", "yin", "ying", "yo", |
||||
"yong", "you", "yu", "yuan", "yue", "yun", "za", "zai", "zan", "zang", "zao", "ze", "zei", "zen", "zeng", "zha", "zhai", "zhan", "zhang", "zhao", "zhe", "zhen", "zheng", "zhi", "zhong", |
||||
"zhou", "zhu", "zhua", "zhuai", "zhuan", "zhuang", "zhui", "zhun", "zhuo", "zi", "zong", "zou", "zu", "zuan", "zui", "zun", "zuo" }; |
||||
|
||||
/** |
||||
* 获取所给中文的每个汉字首字母组成首字母字符串 |
||||
* |
||||
* @param chinese 汉字字符串 |
||||
* @return 首字母字符串 |
||||
*/ |
||||
public static String getAllFirstLetter(String chinese) { |
||||
if (StrUtil.isBlank(chinese)) { |
||||
return StrUtil.EMPTY; |
||||
} |
||||
|
||||
int len = chinese.length(); |
||||
final StrBuilder strBuilder = new StrBuilder(len); |
||||
for (int i = 0; i < len; i++) { |
||||
strBuilder.append(getFirstLetter(chinese.charAt(i))); |
||||
} |
||||
|
||||
return strBuilder.toString(); |
||||
} |
||||
|
||||
/** |
||||
* 获取拼音首字母<br> |
||||
* 传入汉字,返回拼音首字母<br> |
||||
* 如果传入为字母,返回其小写形式<br> |
||||
* 感谢【帝都】宁静 提供方法 |
||||
* |
||||
* @param ch 汉字 |
||||
* @return 首字母,小写 |
||||
*/ |
||||
public static char getFirstLetter(char ch) { |
||||
if (ch >= 'a' && ch <= 'z') { |
||||
return ch; |
||||
} |
||||
if (ch >= 'A' && ch <= 'Z') { |
||||
return Character.toLowerCase(ch); |
||||
} |
||||
final byte[] bys = String.valueOf(ch).getBytes(CharsetUtil.CHARSET_GBK); |
||||
if (bys.length == 1) { |
||||
return ch; |
||||
} |
||||
int count = (bys[0] + 256) * 256 + bys[1] + 256; |
||||
if (count < 45217) { |
||||
return ch; |
||||
} else if (count < 45253) { |
||||
return 'a'; |
||||
} else if (count < 45761) { |
||||
return 'b'; |
||||
} else if (count < 46318) { |
||||
return 'c'; |
||||
} else if (count < 46826) { |
||||
return 'd'; |
||||
} else if (count < 47010) { |
||||
return 'e'; |
||||
} else if (count < 47297) { |
||||
return 'f'; |
||||
} else if (count < 47614) { |
||||
return 'g'; |
||||
} else if (count < 48119) { |
||||
return 'h'; |
||||
} else if (count < 49062) { |
||||
return 'j'; |
||||
} else if (count < 49324) { |
||||
return 'k'; |
||||
} else if (count < 49896) { |
||||
return 'l'; |
||||
} else if (count < 50371) { |
||||
return 'm'; |
||||
} else if (count < 50614) { |
||||
return 'n'; |
||||
} else if (count < 50622) { |
||||
return 'o'; |
||||
} else if (count < 50906) { |
||||
return 'p'; |
||||
} else if (count < 51387) { |
||||
return 'q'; |
||||
} else if (count < 51446) { |
||||
return 'r'; |
||||
} else if (count < 52218) { |
||||
return 's'; |
||||
} else if (count < 52698) { |
||||
return 't'; |
||||
} else if (count < 52980) { |
||||
return 'w'; |
||||
} else if (count < 53689) { |
||||
return 'x'; |
||||
} else if (count < 54481) { |
||||
return 'y'; |
||||
} else if (count < 55290) { |
||||
return 'z'; |
||||
} |
||||
return ch; |
||||
} |
||||
|
||||
/** |
||||
* 汉字转拼音 |
||||
* <hr> |
||||
* example : 张三 zhangsan |
||||
* |
||||
* @param chinese 汉字 |
||||
* @return 对应的拼音 |
||||
* @since 4.0.11 |
||||
*/ |
||||
public static String getPinYin(String chinese) { |
||||
final StrBuilder result = StrUtil.strBuilder(); |
||||
String strTemp = null; |
||||
int len = chinese.length(); |
||||
for (int j = 0; j < len; j++) { |
||||
strTemp = chinese.substring(j, j + 1); |
||||
int ascii = getChsAscii(strTemp); |
||||
if (ascii > 0) { |
||||
//非汉字
|
||||
result.append((char)ascii); |
||||
} else { |
||||
for (int i = pinyinValue.length - 1; i >= 0; i--) { |
||||
if (pinyinValue[i] <= ascii) { |
||||
result.append(pinyinStr[i]); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return result.toString(); |
||||
} |
||||
|
||||
//------------------------------------------------------------------------------------------------------- Private method start
|
||||
/** |
||||
* 获取汉字对应的ascii码 |
||||
* @param chs 汉字 |
||||
* @return ascii码 |
||||
*/ |
||||
private static int getChsAscii(String chs) { |
||||
int asc = 0; |
||||
byte[] bytes = chs.getBytes(CharsetUtil.CHARSET_GBK); |
||||
switch (bytes.length) { |
||||
case 1: |
||||
// 英文字符
|
||||
asc = bytes[0]; |
||||
break; |
||||
case 2: |
||||
// 中文字符
|
||||
int hightByte = 256 + bytes[0]; |
||||
int lowByte = 256 + bytes[1]; |
||||
asc = (256 * hightByte + lowByte) - 256 * 256; |
||||
break; |
||||
default: |
||||
throw new UtilException("Illegal resource string"); |
||||
} |
||||
return asc; |
||||
} |
||||
} |
@ -0,0 +1,87 @@
@@ -0,0 +1,87 @@
|
||||
package com.cloud.kicc.common.core.util; |
||||
|
||||
import lombok.SneakyThrows; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.DisposableBean; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.ApplicationContextAware; |
||||
import org.springframework.context.ApplicationEvent; |
||||
import org.springframework.context.annotation.Lazy; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
/** |
||||
*<p> |
||||
* Spring工具类 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@Slf4j |
||||
@Service |
||||
@Lazy(false) |
||||
public class SpringContextHolderUtil implements ApplicationContextAware, DisposableBean { |
||||
|
||||
private static ApplicationContext applicationContext = null; |
||||
|
||||
/** |
||||
* 取得存储在静态变量中的ApplicationContext. |
||||
*/ |
||||
public static ApplicationContext getApplicationContext() { |
||||
return applicationContext; |
||||
} |
||||
|
||||
/** |
||||
* 实现ApplicationContextAware接口, 注入Context到静态变量中. |
||||
*/ |
||||
@Override |
||||
public void setApplicationContext(ApplicationContext applicationContext) { |
||||
SpringContextHolderUtil.applicationContext = applicationContext; |
||||
} |
||||
|
||||
/** |
||||
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public static <T> T getBean(String name) { |
||||
return (T) applicationContext.getBean(name); |
||||
} |
||||
|
||||
/** |
||||
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. |
||||
*/ |
||||
public static <T> T getBean(Class<T> requiredType) { |
||||
return applicationContext.getBean(requiredType); |
||||
} |
||||
|
||||
/** |
||||
* 清除SpringContextHolder中的ApplicationContext为Null. |
||||
*/ |
||||
public static void clearHolder() { |
||||
if (log.isDebugEnabled()) { |
||||
log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext); |
||||
} |
||||
applicationContext = null; |
||||
} |
||||
|
||||
/** |
||||
* 发布事件 |
||||
* @param event |
||||
*/ |
||||
public static void publishEvent(ApplicationEvent event) { |
||||
if (applicationContext == null) { |
||||
return; |
||||
} |
||||
applicationContext.publishEvent(event); |
||||
} |
||||
|
||||
/** |
||||
* 实现DisposableBean接口, 在Context关闭时清理静态变量. |
||||
*/ |
||||
@Override |
||||
@SneakyThrows |
||||
public void destroy() { |
||||
SpringContextHolderUtil.clearHolder(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,321 @@
@@ -0,0 +1,321 @@
|
||||
package com.cloud.kicc.common.core.util; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.Date; |
||||
|
||||
/** |
||||
*<p> |
||||
* 时间计算工具类 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/1/15 |
||||
*/ |
||||
public class TimeUtils { |
||||
|
||||
public static String toTimeString(long time) { |
||||
TimeUtils t = new TimeUtils(time); |
||||
int day = t.get(TimeUtils.DAY); |
||||
int hour = t.get(TimeUtils.HOUR); |
||||
int minute = t.get(TimeUtils.MINUTE); |
||||
int second = t.get(TimeUtils.SECOND); |
||||
StringBuilder sb = new StringBuilder(); |
||||
if (day > 0){ |
||||
sb.append(day).append("天"); |
||||
} |
||||
if (hour > 0){ |
||||
sb.append(hour).append("时"); |
||||
} |
||||
if (minute > 0){ |
||||
sb.append(minute).append("分"); |
||||
} |
||||
if (second > 0){ |
||||
sb.append(second).append("秒"); |
||||
} |
||||
return sb.toString(); |
||||
} |
||||
|
||||
/** |
||||
* 时间字段常量,表示“秒” |
||||
*/ |
||||
public final static int SECOND = 0; |
||||
|
||||
/** |
||||
* 时间字段常量,表示“分” |
||||
*/ |
||||
public final static int MINUTE = 1; |
||||
|
||||
/** |
||||
* 时间字段常量,表示“时” |
||||
*/ |
||||
public final static int HOUR = 2; |
||||
|
||||
/** |
||||
* 时间字段常量,表示“天” |
||||
*/ |
||||
public final static int DAY = 3; |
||||
|
||||
/** |
||||
* 各常量允许的最大值 |
||||
*/ |
||||
private final int[] maxFields = { 59, 59, 23, Integer.MAX_VALUE - 1 }; |
||||
|
||||
/** |
||||
* 各常量允许的最小值 |
||||
*/ |
||||
private final int[] minFields = { 0, 0, 0, Integer.MIN_VALUE }; |
||||
|
||||
/** |
||||
* 默认的字符串格式时间分隔符 |
||||
*/ |
||||
private String timeSeparator = ":"; |
||||
|
||||
/** |
||||
* 时间数据容器 |
||||
*/ |
||||
private int[] fields = new int[4]; |
||||
|
||||
/** |
||||
* 无参构造,将各字段置为 0 |
||||
*/ |
||||
public TimeUtils() { |
||||
this(0, 0, 0, 0); |
||||
} |
||||
|
||||
/** |
||||
* 使用时、分构造一个时间 |
||||
* @param hour 小时 |
||||
* @param minute 分钟 |
||||
*/ |
||||
public TimeUtils(int hour, int minute) { |
||||
this(0, hour, minute, 0); |
||||
} |
||||
|
||||
/** |
||||
* 使用时、分、秒构造一个时间 |
||||
* @param hour 小时 |
||||
* @param minute 分钟 |
||||
* @param second 秒 |
||||
*/ |
||||
public TimeUtils(int hour, int minute, int second) { |
||||
this(0, hour, minute, second); |
||||
} |
||||
|
||||
/** |
||||
* 使用一个字符串构造时间<br/> |
||||
* Time time = new Time("14:22:23"); |
||||
* @param time 字符串格式的时间,默认采用“:”作为分隔符 |
||||
*/ |
||||
public TimeUtils(String time) { |
||||
this(time, null); |
||||
// System.out.println(time);
|
||||
} |
||||
|
||||
/** |
||||
* 使用时间毫秒构建时间 |
||||
* @param time |
||||
*/ |
||||
public TimeUtils(long time){ |
||||
this(new Date(time)); |
||||
} |
||||
/** |
||||
* 使用日期对象构造时间 |
||||
* @param date |
||||
*/ |
||||
public TimeUtils(Date date){ |
||||
this(DateUtil.formatUTC(date, "HH:mm:ss")); |
||||
} |
||||
|
||||
/** |
||||
* 使用天、时、分、秒构造时间,进行全字符的构造 |
||||
* @param day 天 |
||||
* @param hour 时 |
||||
* @param minute 分 |
||||
* @param second 秒 |
||||
*/ |
||||
public TimeUtils(int day, int hour, int minute, int second) { |
||||
initialize(day, hour, minute, second); |
||||
} |
||||
|
||||
/** |
||||
* 使用一个字符串构造时间,指定分隔符<br/> |
||||
* Time time = new Time("14-22-23", "-"); |
||||
* @param time 字符串格式的时间 |
||||
*/ |
||||
public TimeUtils(String time, String timeSeparator) { |
||||
if(timeSeparator != null) { |
||||
setTimeSeparator(timeSeparator); |
||||
} |
||||
parseTime(time); |
||||
} |
||||
|
||||
/** |
||||
* 设置时间字段的值 |
||||
* @param field 时间字段常量 |
||||
* @param value 时间字段的值 |
||||
*/ |
||||
public void set(int field, int value) { |
||||
if(value < minFields[field]) { |
||||
throw new IllegalArgumentException(value + ", time value must be positive."); |
||||
} |
||||
fields[field] = value % (maxFields[field] + 1); |
||||
// 进行进位计算
|
||||
int carry = value / (maxFields[field] + 1); |
||||
if(carry > 0) { |
||||
int upFieldValue = get(field + 1); |
||||
set(field + 1, upFieldValue + carry); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获得时间字段的值 |
||||
* @param field 时间字段常量 |
||||
* @return 该时间字段的值 |
||||
*/ |
||||
public int get(int field) { |
||||
if(field < 0 || field > fields.length - 1) { |
||||
throw new IllegalArgumentException(field + ", field value is error."); |
||||
} |
||||
return fields[field]; |
||||
} |
||||
|
||||
/** |
||||
* 将时间进行“加”运算,即加上一个时间 |
||||
* @param time 需要加的时间 |
||||
* @return 运算后的时间 |
||||
*/ |
||||
public TimeUtils addTime(TimeUtils time) { |
||||
TimeUtils result = new TimeUtils(); |
||||
int up = 0; // 进位标志
|
||||
for (int i = 0; i < fields.length; i++) { |
||||
int sum = fields[i] + time.fields[i] + up; |
||||
up = sum / (maxFields[i] + 1); |
||||
result.fields[i] = sum % (maxFields[i] + 1); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* 将时间进行“减”运算,即减去一个时间 |
||||
* @param time 需要减的时间 |
||||
* @return 运算后的时间 |
||||
*/ |
||||
public TimeUtils subtractTime(TimeUtils time) { |
||||
TimeUtils result = new TimeUtils(); |
||||
int down = 0; // 退位标志
|
||||
for (int i = 0, k = fields.length - 1; i < k; i++) { |
||||
int difference = fields[i] + down; |
||||
if (difference >= time.fields[i]) { |
||||
difference -= time.fields[i]; |
||||
down = 0; |
||||
} else { |
||||
difference += maxFields[i] + 1 - time.fields[i]; |
||||
down = -1; |
||||
} |
||||
result.fields[i] = difference; |
||||
} |
||||
result.fields[DAY] = fields[DAY] - time.fields[DAY] + down; |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* 获得时间字段的分隔符 |
||||
* @return |
||||
*/ |
||||
public String getTimeSeparator() { |
||||
return timeSeparator; |
||||
} |
||||
|
||||
/** |
||||
* 设置时间字段的分隔符(用于字符串格式的时间) |
||||
* @param timeSeparator 分隔符字符串 |
||||
*/ |
||||
public void setTimeSeparator(String timeSeparator) { |
||||
this.timeSeparator = timeSeparator; |
||||
} |
||||
|
||||
private void initialize(int day, int hour, int minute, int second) { |
||||
set(DAY, day); |
||||
set(HOUR, hour); |
||||
set(MINUTE, minute); |
||||
set(SECOND, second); |
||||
} |
||||
|
||||
private void parseTime(String time) { |
||||
if(time == null) { |
||||
initialize(0, 0, 0, 0); |
||||
return; |
||||
} |
||||
String t = time; |
||||
int field = DAY; |
||||
set(field--, 0); |
||||
int p = -1; |
||||
while((p = t.indexOf(timeSeparator)) > -1) { |
||||
parseTimeField(time, t.substring(0, p), field--); |
||||
t = t.substring(p + timeSeparator.length()); |
||||
} |
||||
parseTimeField(time, t, field--); |
||||
} |
||||
|
||||
private void parseTimeField(String time, String t, int field) { |
||||
if(field < SECOND || t.length() < 1) { |
||||
parseTimeException(time); |
||||
} |
||||
char[] chs = t.toCharArray(); |
||||
int n = 0; |
||||
for(int i = 0; i < chs.length; i++) { |
||||
if(chs[i] <= ' ') { |
||||
continue; |
||||
} |
||||
if(chs[i] >= '0' && chs[i] <= '9') { |
||||
n = n * 10 + chs[i] - '0'; |
||||
continue; |
||||
} |
||||
parseTimeException(time); |
||||
} |
||||
set(field, n); |
||||
} |
||||
|
||||
private void parseTimeException(String time) { |
||||
throw new IllegalArgumentException(time + ", time format error, HH" |
||||
+ this.timeSeparator + "mm" + this.timeSeparator + "ss"); |
||||
} |
||||
|
||||
public String toString() { |
||||
StringBuilder sb = new StringBuilder(16); |
||||
sb.append(fields[DAY]).append(',').append(' '); |
||||
buildString(sb, HOUR).append(timeSeparator); |
||||
buildString(sb, MINUTE).append(timeSeparator); |
||||
buildString(sb, SECOND); |
||||
return sb.toString(); |
||||
} |
||||
|
||||
private StringBuilder buildString(StringBuilder sb, int field) { |
||||
if(fields[field] < 10) { |
||||
sb.append('0'); |
||||
} |
||||
return sb.append(fields[field]); |
||||
} |
||||
|
||||
public int hashCode() { |
||||
final int PRIME = 31; |
||||
int result = 1; |
||||
result = PRIME * result + Arrays.hashCode(fields); |
||||
return result; |
||||
} |
||||
|
||||
public boolean equals(Object obj) { |
||||
if (this == obj) |
||||
return true; |
||||
if (obj == null) |
||||
return false; |
||||
if (getClass() != obj.getClass()) |
||||
return false; |
||||
final TimeUtils other = (TimeUtils) obj; |
||||
if (!Arrays.equals(fields, other.fields)) { |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,186 @@
@@ -0,0 +1,186 @@
|
||||
package com.cloud.kicc.common.core.util; |
||||
|
||||
import cn.hutool.core.codec.Base64; |
||||
import cn.hutool.json.JSONUtil; |
||||
import com.cloud.kicc.common.core.constant.CommonConstants; |
||||
import com.cloud.kicc.common.core.exception.CheckedException; |
||||
import lombok.SneakyThrows; |
||||
import lombok.experimental.UtilityClass; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||
import org.springframework.web.bind.annotation.ResponseBody; |
||||
import org.springframework.web.context.request.RequestContextHolder; |
||||
import org.springframework.web.context.request.ServletRequestAttributes; |
||||
import org.springframework.web.method.HandlerMethod; |
||||
|
||||
import javax.servlet.http.Cookie; |
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
import javax.validation.constraints.NotNull; |
||||
import java.io.IOException; |
||||
import java.io.PrintWriter; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.Optional; |
||||
|
||||
/** |
||||
*<p> |
||||
* 扩展用于 Web 应用程序的各种实用程序工具类 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@Slf4j |
||||
@UtilityClass |
||||
public class WebUtil extends org.springframework.web.util.WebUtils { |
||||
|
||||
private final String BASIC_ = "Basic "; |
||||
|
||||
private final String UNKNOWN = "unknown"; |
||||
|
||||
/** |
||||
* 判断是否ajax请求 spring ajax 返回含有 ResponseBody 或者 RestController注解 |
||||
* @param handlerMethod HandlerMethod |
||||
* @return 是否ajax请求 |
||||
*/ |
||||
public boolean isBody(HandlerMethod handlerMethod) { |
||||
ResponseBody responseBody = ClassUtil.getAnnotation(handlerMethod, ResponseBody.class); |
||||
return responseBody != null; |
||||
} |
||||
|
||||
/** |
||||
* 读取cookie |
||||
* @param name cookie name |
||||
* @return cookie value |
||||
*/ |
||||
public String getCookieVal(String name) { |
||||
if (WebUtil.getRequest().isPresent()) { |
||||
return getCookieVal(WebUtil.getRequest().get(), name); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* 读取cookie |
||||
* @param request HttpServletRequest |
||||
* @param name cookie name |
||||
* @return cookie value |
||||
*/ |
||||
public String getCookieVal(HttpServletRequest request, String name) { |
||||
Cookie cookie = getCookie(request, name); |
||||
return cookie != null ? cookie.getValue() : null; |
||||
} |
||||
|
||||
/** |
||||
* 清除 某个指定的cookie |
||||
* @param response HttpServletResponse |
||||
* @param key cookie key |
||||
*/ |
||||
public void removeCookie(HttpServletResponse response, String key) { |
||||
setCookie(response, key, null, 0); |
||||
} |
||||
|
||||
/** |
||||
* 设置cookie |
||||
* @param response HttpServletResponse |
||||
* @param name cookie name |
||||
* @param value cookie value |
||||
* @param maxAgeInSeconds maxage |
||||
*/ |
||||
public void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds) { |
||||
Cookie cookie = new Cookie(name, value); |
||||
cookie.setPath("/"); |
||||
cookie.setMaxAge(maxAgeInSeconds); |
||||
cookie.setHttpOnly(true); |
||||
response.addCookie(cookie); |
||||
} |
||||
|
||||
/** |
||||
* 获取 HttpServletRequest |
||||
* @return {HttpServletRequest} |
||||
*/ |
||||
public Optional<HttpServletRequest> getRequest() { |
||||
return Optional |
||||
.ofNullable(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()); |
||||
} |
||||
|
||||
/** |
||||
* 获取 HttpServletResponse |
||||
* @return {HttpServletResponse} |
||||
*/ |
||||
public HttpServletResponse getResponse() { |
||||
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); |
||||
} |
||||
|
||||
/** |
||||
* 返回json |
||||
* @param response HttpServletResponse |
||||
* @param result 结果对象 |
||||
*/ |
||||
public void renderJson(HttpServletResponse response, Object result) { |
||||
renderJson(response, result, MediaType.APPLICATION_JSON_VALUE); |
||||
} |
||||
|
||||
/** |
||||
* 返回json |
||||
* @param response HttpServletResponse |
||||
* @param result 结果对象 |
||||
* @param contentType contentType |
||||
*/ |
||||
public void renderJson(HttpServletResponse response, Object result, String contentType) { |
||||
response.setCharacterEncoding(CommonConstants.UTF8); |
||||
response.setContentType(contentType); |
||||
try (PrintWriter out = response.getWriter()) { |
||||
out.append(JSONUtil.toJsonStr(result)); |
||||
} |
||||
catch (IOException e) { |
||||
log.error(e.getMessage(), e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 从request 获取CLIENT_ID |
||||
* @return |
||||
*/ |
||||
@SneakyThrows |
||||
public String getClientId(ServerHttpRequest request) { |
||||
String header = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); |
||||
return splitClient(header)[0]; |
||||
} |
||||
|
||||
@SneakyThrows |
||||
public String getClientId() { |
||||
if (WebUtil.getRequest().isPresent()) { |
||||
String header = WebUtil.getRequest().get().getHeader(HttpHeaders.AUTHORIZATION); |
||||
return splitClient(header)[0]; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
@NotNull |
||||
private static String[] splitClient(String header) { |
||||
if (header == null || !header.startsWith(BASIC_)) { |
||||
throw new CheckedException("请求头中client信息为空!"); |
||||
} |
||||
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8); |
||||
byte[] decoded; |
||||
try { |
||||
decoded = Base64.decode(base64Token); |
||||
} |
||||
catch (IllegalArgumentException e) { |
||||
throw new CheckedException("Failed to decode basic authentication token"); |
||||
} |
||||
|
||||
String token = new String(decoded, StandardCharsets.UTF_8); |
||||
|
||||
int delim = token.indexOf(":"); |
||||
|
||||
if (delim == -1) { |
||||
throw new CheckedException("Invalid basic authentication token"); |
||||
} |
||||
return new String[] { token.substring(0, delim), token.substring(delim + 1) }; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
||||
com.cloud.kicc.common.core.config.WebMvcConfiguration,\ |
||||
com.cloud.kicc.common.core.config.RestTemplateConfiguration,\ |
||||
com.cloud.kicc.common.core.config.GatewayConfigProperties,\ |
||||
com.cloud.kicc.common.core.util.SpringContextHolderUtil |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
${AnsiColor.BRIGHT_GREEN} |
||||
|
||||
___ __ ___ ________ ________ |
||||
|\ \|\ \ |\ \|\ ____\|\ ____\ |
||||
\ \ \/ /|\ \ \ \ \___|\ \ \___| |
||||
\ \ ___ \ \ \ \ \ \ \ \ |
||||
\ \ \\ \ \ \ \ \ \____\ \ \____ |
||||
\ \__\\ \__\ \__\ \_______\ \_______\ |
||||
\|__| \|__|\|__|\|_______|\|_______| |
||||
|
||||
www.kanglailab.com |
||||
|
||||
长沙康来生物有限公司 Microservice Architecture |
||||
${AnsiColor.DEFAULT} |
||||
|
||||
|
||||
|
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!--基础日志配置,工程继承可以进行替换--> |
||||
<configuration debug="false" scan="false"> |
||||
<springProperty scop="context" name="spring.application.name" source="spring.application.name" defaultValue=""/> |
||||
<property name="log.path" value="logs/${spring.application.name}"/> |
||||
<!-- 彩色日志格式 --> |
||||
<property name="CONSOLE_LOG_PATTERN" |
||||
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> |
||||
<!-- 彩色日志依赖的渲染类 --> |
||||
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/> |
||||
<conversionRule conversionWord="wex" |
||||
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/> |
||||
<conversionRule conversionWord="wEx" |
||||
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/> |
||||
<!-- 控制台日志输出 --> |
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender"> |
||||
<encoder> |
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern> |
||||
</encoder> |
||||
</appender> |
||||
|
||||
<!-- 日志文件调试输出 --> |
||||
<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender"> |
||||
<file>${log.path}/debug.log</file> |
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> |
||||
<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> |
||||
<maxFileSize>50MB</maxFileSize> |
||||
<maxHistory>30</maxHistory> |
||||
</rollingPolicy> |
||||
<encoder> |
||||
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern> |
||||
</encoder> |
||||
</appender> |
||||
|
||||
<!-- 日志文件错误输出 --> |
||||
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender"> |
||||
<file>${log.path}/error.log</file> |
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> |
||||
<fileNamePattern>${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> |
||||
<maxFileSize>50MB</maxFileSize> |
||||
<maxHistory>30</maxHistory> |
||||
</rollingPolicy> |
||||
<encoder> |
||||
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern> |
||||
</encoder> |
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter"> |
||||
<level>ERROR</level> |
||||
</filter> |
||||
</appender> |
||||
|
||||
<!-- Level: FATAL 0 ERROR 3 WARN 4 INFO 6 DEBUG 7 --> |
||||
<root level="INFO"> |
||||
<appender-ref ref="console"/> |
||||
<appender-ref ref="debug"/> |
||||
<appender-ref ref="error"/> |
||||
</root> |
||||
</configuration> |
@ -0,0 +1,67 @@
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-tool</artifactId> |
||||
<version>1.0.0</version> |
||||
</parent> |
||||
|
||||
<artifactId>kicc-common-data</artifactId> |
||||
<packaging>jar</packaging> |
||||
|
||||
<description>kicc 数据服务核心包</description> |
||||
|
||||
<!--考虑这个作为一个单模块使用,目前依赖了工具类核心包,后续引入依赖需要注意低耦合--> |
||||
<dependencies> |
||||
<!--数据服务核心包--> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-core</artifactId> |
||||
</dependency> |
||||
<!--支持jackson注解--> |
||||
<dependency> |
||||
<groupId>com.fasterxml.jackson.core</groupId> |
||||
<artifactId>jackson-annotations</artifactId> |
||||
</dependency> |
||||
<!--redis--> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-data-redis</artifactId> |
||||
</dependency> |
||||
<!--mybatis-plus--> |
||||
<dependency> |
||||
<groupId>com.baomidou</groupId> |
||||
<artifactId>mybatis-plus-boot-starter</artifactId> |
||||
</dependency> |
||||
<!--数据库--> |
||||
<dependency> |
||||
<groupId>mysql</groupId> |
||||
<artifactId>mysql-connector-java</artifactId> |
||||
</dependency> |
||||
<!--数据源管理需要哪个就开启哪个--> |
||||
<!--Oracle--> |
||||
<!--<dependency> |
||||
<groupId>com.oracle</groupId> |
||||
<artifactId>ojdbc7</artifactId> |
||||
<version>${oracle.version}</version> |
||||
</dependency>--> |
||||
<!--postgresql--> |
||||
<!--<dependency> |
||||
<groupId>org.postgresql</groupId> |
||||
<artifactId>postgresql</artifactId> |
||||
</dependency>--> |
||||
<!--对外部api提供mvc相关服务--> |
||||
<dependency> |
||||
<groupId>org.springframework</groupId> |
||||
<artifactId>spring-webmvc</artifactId> |
||||
</dependency> |
||||
<!--security 依赖--> |
||||
<dependency> |
||||
<groupId>org.springframework.security</groupId> |
||||
<artifactId>spring-security-core</artifactId> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
package com.cloud.kicc.common.data.annotation; |
||||
|
||||
import com.cloud.kicc.common.data.config.MybatisConfiguration; |
||||
import com.cloud.kicc.common.data.config.RedisTemplateConfiguration; |
||||
import org.springframework.context.annotation.Import; |
||||
|
||||
import java.lang.annotation.*; |
||||
|
||||
/** |
||||
*<p> |
||||
* 开启数据存储和访问层 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Since: 2023/9/15 |
||||
*/ |
||||
@Target({ ElementType.TYPE }) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Documented |
||||
@Inherited |
||||
@Import({ MybatisConfiguration.class, RedisTemplateConfiguration.class }) |
||||
public @interface EnableKiccDataRepository { |
||||
|
||||
} |
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
package com.cloud.kicc.common.data.config; |
||||
|
||||
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer; |
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; |
||||
import com.baomidou.mybatisplus.extension.MybatisMapWrapperFactory; |
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; |
||||
import com.cloud.kicc.common.core.factory.YamlPropertySourceFactory; |
||||
import com.cloud.kicc.common.data.handler.BaseMetaObjectHandler; |
||||
import com.cloud.kicc.common.data.handler.KiccTenantLineHandler; |
||||
import com.cloud.kicc.common.data.plugins.KiccPaginationInnerInterceptor; |
||||
import com.cloud.kicc.common.data.plugins.KiccTenantLineInnerInterceptor; |
||||
import com.cloud.kicc.common.data.properties.TenantProperties; |
||||
import lombok.RequiredArgsConstructor; |
||||
import org.mybatis.spring.annotation.MapperScan; |
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.annotation.PropertySource; |
||||
|
||||
/** |
||||
*<p> |
||||
* mybatis plus 统一配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@Configuration(proxyBeanMethods = false) |
||||
@MapperScan("com.cloud.kicc.**.mapper") |
||||
@EnableConfigurationProperties(TenantProperties.class) |
||||
@PropertySource(factory = YamlPropertySourceFactory.class, value = "classpath:kicc-tenant.yml") |
||||
@RequiredArgsConstructor |
||||
public class MybatisConfiguration { |
||||
|
||||
private final TenantProperties tenantProperties; |
||||
|
||||
/** |
||||
* 插件配置 |
||||
* 多租户插件, 自动拼接多租户id进行增删改查 |
||||
* 分页插件, 对于单一数据库类型来说,都建议配置该值,避免每次分页都去抓取数据库类型 |
||||
*/ |
||||
@Bean |
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() { |
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); |
||||
interceptor.addInnerInterceptor(new KiccTenantLineInnerInterceptor(new KiccTenantLineHandler(tenantProperties))); |
||||
interceptor.addInnerInterceptor(new KiccPaginationInnerInterceptor()); |
||||
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); |
||||
return interceptor; |
||||
} |
||||
|
||||
/** |
||||
* 审计字段自动填充 |
||||
* @return {@link MetaObjectHandler} |
||||
*/ |
||||
@Bean |
||||
public BaseMetaObjectHandler mybatisPlusMetaObjectHandler() { |
||||
return new BaseMetaObjectHandler(); |
||||
} |
||||
|
||||
@Bean |
||||
public ConfigurationCustomizer mybatisConfigurationCustomizer(){ |
||||
return configuration -> configuration.setObjectWrapperFactory(new MybatisMapWrapperFactory()); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
package com.cloud.kicc.common.data.config; |
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
||||
import org.springframework.cache.annotation.EnableCaching; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
||||
import org.springframework.data.redis.core.*; |
||||
import org.springframework.data.redis.serializer.RedisSerializer; |
||||
|
||||
/** |
||||
*<p> |
||||
* Redis 配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@EnableCaching |
||||
@Configuration(proxyBeanMethods = false) |
||||
public class RedisTemplateConfiguration { |
||||
|
||||
@Bean |
||||
@ConditionalOnMissingBean |
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { |
||||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); |
||||
redisTemplate.setKeySerializer(RedisSerializer.string()); |
||||
redisTemplate.setHashKeySerializer(RedisSerializer.string()); |
||||
redisTemplate.setValueSerializer(RedisSerializer.java()); |
||||
redisTemplate.setHashValueSerializer(RedisSerializer.java()); |
||||
redisTemplate.setConnectionFactory(factory); |
||||
return redisTemplate; |
||||
} |
||||
|
||||
@Bean |
||||
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) { |
||||
return redisTemplate.opsForHash(); |
||||
} |
||||
|
||||
@Bean |
||||
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) { |
||||
return redisTemplate.opsForValue(); |
||||
} |
||||
|
||||
@Bean |
||||
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) { |
||||
return redisTemplate.opsForList(); |
||||
} |
||||
|
||||
@Bean |
||||
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) { |
||||
return redisTemplate.opsForSet(); |
||||
} |
||||
|
||||
@Bean |
||||
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) { |
||||
return redisTemplate.opsForZSet(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
package com.cloud.kicc.common.data.entity; |
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField; |
||||
import com.fasterxml.jackson.annotation.JsonIgnore; |
||||
import io.swagger.annotations.ApiModelProperty; |
||||
import lombok.Data; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
/** |
||||
*<p> |
||||
* 基础模型 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2021/12/28 |
||||
*/ |
||||
@Data |
||||
public class BaseEntity implements Serializable { |
||||
|
||||
protected static final long serialVersionUID = 1L; |
||||
|
||||
/** 多租户ID */ |
||||
@ApiModelProperty("多租户ID") |
||||
protected String tenantId; |
||||
|
||||
/** 当前用户 */ |
||||
@ApiModelProperty("当前用户") |
||||
@TableField(exist = false) |
||||
protected KiccUser currentUser; |
||||
|
||||
/** 自定义sql过滤 */ |
||||
@TableField(exist = false) |
||||
@JsonIgnore |
||||
private String sqlFilter; |
||||
|
||||
} |
@ -0,0 +1,185 @@
@@ -0,0 +1,185 @@
|
||||
package com.cloud.kicc.common.data.entity; |
||||
|
||||
import lombok.EqualsAndHashCode; |
||||
import lombok.Getter; |
||||
import lombok.Setter; |
||||
import lombok.experimental.Accessors; |
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import org.springframework.security.core.userdetails.User; |
||||
|
||||
import java.time.LocalDateTime; |
||||
import java.util.Collection; |
||||
import java.util.Map; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
|
||||
/** |
||||
*<p> |
||||
* CAS统一认证用户数据 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Since: 2023/8/16 |
||||
*/ |
||||
@Setter |
||||
@Getter |
||||
@Accessors(chain = true) |
||||
@EqualsAndHashCode(callSuper = false) |
||||
public class CasUser extends User { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
/** 用户ID */ |
||||
private String id; |
||||
|
||||
/** 昵称 */ |
||||
private String nickName; |
||||
|
||||
/** 邮箱 */ |
||||
private String email; |
||||
|
||||
/** 手机号 */ |
||||
private String phone; |
||||
|
||||
/** 性别 */ |
||||
private String sex; |
||||
|
||||
/** 头像地址 */ |
||||
private String avatar; |
||||
|
||||
/** 最后登陆ip */ |
||||
private String loginIp; |
||||
|
||||
/** 最后登陆时间 */ |
||||
private LocalDateTime loginTime; |
||||
|
||||
/** 状态 */ |
||||
private String ssoStatus; |
||||
|
||||
/** 创建ID */ |
||||
private String ssoCreateById; |
||||
|
||||
/** 创建人 */ |
||||
private String ssoCreateByName; |
||||
|
||||
/** 创建时间 */ |
||||
private LocalDateTime ssoCreateTime; |
||||
|
||||
/** 更新ID */ |
||||
private String ssoUpdateById; |
||||
|
||||
/** 更新人 */ |
||||
private String ssoUpdateByName; |
||||
|
||||
/** 更新时间 */ |
||||
private LocalDateTime ssoUpdateTime; |
||||
|
||||
/** 备注 */ |
||||
private String remarks; |
||||
|
||||
/** 角色ID */ |
||||
private String roleId; |
||||
|
||||
/** 多租户ID */ |
||||
private String tenantId; |
||||
|
||||
/** sso扩展信息 */ |
||||
private Map<String, String> exPrincipals = new ConcurrentHashMap<>(3); |
||||
|
||||
|
||||
public CasUser(String username, String password, Collection<? extends GrantedAuthority> authorities) { |
||||
super(username, password, authorities); |
||||
} |
||||
|
||||
public CasUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { |
||||
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); |
||||
} |
||||
|
||||
public CasUser(String username, |
||||
String password, |
||||
boolean enabled, |
||||
boolean accountNonExpired, |
||||
boolean credentialsNonExpired, |
||||
boolean accountNonLocked, |
||||
Collection<? extends GrantedAuthority> authorities, |
||||
String id, |
||||
String nickName, |
||||
String email, |
||||
String phone, |
||||
String sex, |
||||
String avatar, |
||||
String loginIp, |
||||
LocalDateTime loginTime, |
||||
String ssoStatus, |
||||
String ssoCreateById, |
||||
String ssoCreateByName, |
||||
LocalDateTime ssoCreateTime, |
||||
String ssoUpdateById, |
||||
String ssoUpdateByName, |
||||
LocalDateTime ssoUpdateTime, |
||||
String remarks) { |
||||
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); |
||||
this.id = id; |
||||
this.nickName = nickName; |
||||
this.email = email; |
||||
this.phone = phone; |
||||
this.sex = sex; |
||||
this.avatar = avatar; |
||||
this.loginIp = loginIp; |
||||
this.loginTime = loginTime; |
||||
this.ssoStatus = ssoStatus; |
||||
this.ssoCreateById = ssoCreateById; |
||||
this.ssoCreateByName = ssoCreateByName; |
||||
this.ssoCreateTime = ssoCreateTime; |
||||
this.ssoUpdateById = ssoUpdateById; |
||||
this.ssoUpdateByName = ssoUpdateByName; |
||||
this.ssoUpdateTime = ssoUpdateTime; |
||||
this.remarks = remarks; |
||||
} |
||||
|
||||
public CasUser(String username, |
||||
String password, |
||||
boolean enabled, |
||||
boolean accountNonExpired, |
||||
boolean credentialsNonExpired, |
||||
boolean accountNonLocked, |
||||
Collection<? extends GrantedAuthority> authorities, |
||||
String id, |
||||
String nickName, |
||||
String email, |
||||
String phone, |
||||
String sex, |
||||
String avatar, |
||||
String loginIp, |
||||
LocalDateTime loginTime, |
||||
String ssoStatus, |
||||
String ssoCreateById, |
||||
String ssoCreateByName, |
||||
LocalDateTime ssoCreateTime, |
||||
String ssoUpdateById, |
||||
String ssoUpdateByName, |
||||
LocalDateTime ssoUpdateTime, |
||||
String remarks, |
||||
String roleId, |
||||
String tenantId) { |
||||
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); |
||||
this.id = id; |
||||
this.nickName = nickName; |
||||
this.email = email; |
||||
this.phone = phone; |
||||
this.sex = sex; |
||||
this.avatar = avatar; |
||||
this.loginIp = loginIp; |
||||
this.loginTime = loginTime; |
||||
this.ssoStatus = ssoStatus; |
||||
this.ssoCreateById = ssoCreateById; |
||||
this.ssoCreateByName = ssoCreateByName; |
||||
this.ssoCreateTime = ssoCreateTime; |
||||
this.ssoUpdateById = ssoUpdateById; |
||||
this.ssoUpdateByName = ssoUpdateByName; |
||||
this.ssoUpdateTime = ssoUpdateTime; |
||||
this.remarks = remarks; |
||||
this.roleId = roleId; |
||||
this.tenantId = tenantId; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
|
||||
package com.cloud.kicc.common.data.entity; |
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill; |
||||
import com.baomidou.mybatisplus.annotation.TableField; |
||||
import com.baomidou.mybatisplus.annotation.TableLogic; |
||||
import com.fasterxml.jackson.annotation.JsonFormat; |
||||
import com.fasterxml.jackson.annotation.JsonIgnore; |
||||
import io.swagger.annotations.ApiModelProperty; |
||||
import lombok.Data; |
||||
import lombok.EqualsAndHashCode; |
||||
|
||||
import java.time.LocalDateTime; |
||||
|
||||
/** |
||||
*<p> |
||||
* 通用模型 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2021/12/28 |
||||
*/ |
||||
@Data |
||||
@EqualsAndHashCode(callSuper = true) |
||||
public class CommonEntity extends BaseEntity { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
/** 创建id */ |
||||
@ApiModelProperty("创建id") |
||||
@TableField(value = "create_by_id", fill = FieldFill.INSERT) |
||||
protected String createById; |
||||
|
||||
/** 创建者 */ |
||||
@ApiModelProperty("创建人") |
||||
@TableField(value = "create_by_name", fill = FieldFill.INSERT) |
||||
protected String createByName; |
||||
|
||||
/** 创建时间 */ |
||||
@ApiModelProperty("创建时间") |
||||
@TableField(value = "create_time", fill = FieldFill.INSERT) |
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
||||
protected LocalDateTime createTime; |
||||
|
||||
/** 更新id */ |
||||
@ApiModelProperty("更新id") |
||||
@TableField(value = "update_by_id", fill = FieldFill.UPDATE) |
||||
protected String updateById; |
||||
|
||||
/** 更新者 */ |
||||
@ApiModelProperty("更新者") |
||||
@TableField(value = "update_by_name", fill = FieldFill.UPDATE) |
||||
protected String updateByName; |
||||
|
||||
/** 更新时间 */ |
||||
@ApiModelProperty("更新时间") |
||||
@TableField(value = "update_time", fill = FieldFill.UPDATE) |
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
||||
protected LocalDateTime updateTime; |
||||
|
||||
/** 备注 */ |
||||
@ApiModelProperty("备注") |
||||
protected String remarks; |
||||
|
||||
/** 删除标志(0代表存在 1代表删除)*/ |
||||
@TableLogic |
||||
@JsonIgnore |
||||
protected String delFlag; |
||||
|
||||
/** 开始时间 */ |
||||
@TableField(exist = false) |
||||
@JsonIgnore |
||||
private String beginTime; |
||||
|
||||
/** 结束时间 */ |
||||
@TableField(exist = false) |
||||
@JsonIgnore |
||||
private String endTime; |
||||
|
||||
} |
@ -0,0 +1,109 @@
@@ -0,0 +1,109 @@
|
||||
package com.cloud.kicc.common.data.entity; |
||||
|
||||
import cn.hutool.core.util.ObjectUtil; |
||||
import com.cloud.kicc.common.core.constant.SecurityConstants; |
||||
import com.fasterxml.jackson.annotation.JsonCreator; |
||||
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
import io.swagger.annotations.ApiModel; |
||||
import io.swagger.annotations.ApiModelProperty; |
||||
import lombok.EqualsAndHashCode; |
||||
import lombok.Getter; |
||||
import lombok.Setter; |
||||
import lombok.experimental.Accessors; |
||||
import org.springframework.security.core.authority.AuthorityUtils; |
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority; |
||||
|
||||
import java.time.LocalDateTime; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
*<p> |
||||
* 扩展用户数据 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Since: 2023/8/16 |
||||
*/ |
||||
@Getter |
||||
@Setter |
||||
@Accessors(chain = true) |
||||
@EqualsAndHashCode(callSuper = false) |
||||
@ApiModel(description = "完整用户信息") |
||||
public class KiccUser extends CasUser { |
||||
|
||||
@ApiModelProperty("扩展用户ID") |
||||
private String id; |
||||
|
||||
@ApiModelProperty("CAS用户ID") |
||||
private String casUserId; |
||||
|
||||
@ApiModelProperty("用户类型 {@link com.cloud.kicc.system.api.enums.UserTypeEnum }") |
||||
private String userType; |
||||
|
||||
@ApiModelProperty("机构ID") |
||||
private String deptId; |
||||
|
||||
@ApiModelProperty("机构名称") |
||||
private String deptName; |
||||
|
||||
@ApiModelProperty("地图标记点位置图片旋转值") |
||||
private Double mapOrientation; |
||||
|
||||
@ApiModelProperty("地图设计器默认中心点位置") |
||||
private String mapCenter; |
||||
|
||||
@ApiModelProperty("帐号状态(0正常 1停用)") |
||||
private String status; |
||||
|
||||
@ApiModelProperty("指定登录后首页跳转") |
||||
private String homePath; |
||||
|
||||
@ApiModelProperty("角色ID集合") |
||||
private String[] roleIds; |
||||
|
||||
@ApiModelProperty("菜单按钮权限") |
||||
private String[] permissions; |
||||
|
||||
@ApiModelProperty("多租户ID集合") |
||||
private String[] tenantIds; |
||||
|
||||
@ApiModelProperty("创建ID") |
||||
private String createById; |
||||
|
||||
@ApiModelProperty("创建人") |
||||
private String createByName; |
||||
|
||||
@ApiModelProperty("创建时间") |
||||
private LocalDateTime createTime; |
||||
|
||||
@ApiModelProperty("更新ID") |
||||
private String updateById; |
||||
|
||||
@ApiModelProperty("更新人") |
||||
private String updateByName; |
||||
|
||||
@ApiModelProperty("更新时间") |
||||
private LocalDateTime updateTime; |
||||
|
||||
public KiccUser() { |
||||
super(SecurityConstants.MOCK_USERNAME, SecurityConstants.MOCK_PASSWORD, AuthorityUtils.createAuthorityList()); |
||||
} |
||||
|
||||
@JsonCreator |
||||
public KiccUser(@JsonProperty("username") String username, |
||||
@JsonProperty("password") String password, |
||||
@JsonProperty("enabled") boolean enabled, |
||||
@JsonProperty("accountNonExpired") boolean accountNonExpired, |
||||
@JsonProperty("credentialsNonExpired") boolean credentialsNonExpired, |
||||
@JsonProperty("accountNonLocked") boolean accountNonLocked, |
||||
@JsonProperty("authorities") List<SimpleGrantedAuthority> authorities) { |
||||
super(ObjectUtil.defaultIfBlank(username, SecurityConstants.MOCK_USERNAME), |
||||
ObjectUtil.defaultIfBlank(password, SecurityConstants.MOCK_PASSWORD), |
||||
enabled, |
||||
accountNonExpired, |
||||
credentialsNonExpired, |
||||
accountNonLocked, |
||||
ObjectUtil.defaultIfNull(authorities, AuthorityUtils.createAuthorityList())); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
package com.cloud.kicc.common.data.entity; |
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField; |
||||
import com.baomidou.mybatisplus.annotation.TableName; |
||||
import com.cloud.kicc.common.data.entity.CommonEntity; |
||||
import io.swagger.annotations.ApiModel; |
||||
import lombok.Data; |
||||
import lombok.EqualsAndHashCode; |
||||
import lombok.experimental.Accessors; |
||||
|
||||
import java.time.LocalDateTime; |
||||
|
||||
/** |
||||
*<p> |
||||
* SSO用户统一表 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Since: 2023/8/6 |
||||
*/ |
||||
@Data |
||||
@EqualsAndHashCode(callSuper = false) |
||||
@Accessors(chain = true) |
||||
@TableName("cas_sso_user") |
||||
@ApiModel(description = "SSO用户表") |
||||
public class SsoUser extends CommonEntity { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
/** 用户ID */ |
||||
private String id; |
||||
|
||||
/** 用户名 */ |
||||
private String userName; |
||||
|
||||
/** 昵称 */ |
||||
private String nickName; |
||||
|
||||
/** 密码 */ |
||||
private String password; |
||||
|
||||
/** 用户邮箱 */ |
||||
private String email; |
||||
|
||||
/** 手机号码 */ |
||||
private String phone; |
||||
|
||||
/** 用户性别(0男 1女 2未知)*/ |
||||
private String sex; |
||||
|
||||
/** 头像路径 */ |
||||
private String avatar; |
||||
|
||||
/** 最后登陆IP */ |
||||
private String loginIp; |
||||
|
||||
/** 帐号状态(0正常 1停用)*/ |
||||
private String status; |
||||
|
||||
/** 最后登陆时间 */ |
||||
private LocalDateTime loginTime; |
||||
|
||||
/** 新密码 */ |
||||
@TableField(exist = false) |
||||
private String newPassword; |
||||
} |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
package com.cloud.kicc.common.data.entity; |
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField; |
||||
import com.baomidou.mybatisplus.annotation.TableId; |
||||
import com.fasterxml.jackson.annotation.JsonInclude; |
||||
import io.swagger.annotations.ApiModelProperty; |
||||
import lombok.Data; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
*<p> |
||||
* 树结构模型 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2021/12/28 |
||||
*/ |
||||
@Data |
||||
public class TreeEntity<T> extends CommonEntity { |
||||
|
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
/** 编号 **/ |
||||
@ApiModelProperty("编号") |
||||
private String id; |
||||
|
||||
/** 父级编号 **/ |
||||
@ApiModelProperty("父级编号") |
||||
private String parentId; |
||||
|
||||
/** 名称 */ |
||||
@ApiModelProperty("名称") |
||||
protected String name; |
||||
|
||||
/** 排序 **/ |
||||
@ApiModelProperty("排序") |
||||
private Integer sort; |
||||
|
||||
@ApiModelProperty("子级集合") |
||||
@TableField(exist = false) |
||||
@JsonInclude(JsonInclude.Include.NON_NULL) |
||||
protected List<T> children; |
||||
|
||||
} |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
package com.cloud.kicc.common.data.enums; |
||||
|
||||
import lombok.AllArgsConstructor; |
||||
import lombok.Getter; |
||||
|
||||
/** |
||||
*<p> |
||||
* 数据类型 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@Getter |
||||
@AllArgsConstructor |
||||
public enum DataTypeEnum { |
||||
/** |
||||
* mysql |
||||
*/ |
||||
MYSQL("mysql", "com.mysql.cj.jdbc.Driver"), |
||||
|
||||
/** |
||||
* sqlserver |
||||
*/ |
||||
SQLSERVER("sqlserver", "com.microsoft.sqlserver.jdbc.SQLServerDriver"), |
||||
|
||||
/** |
||||
* oracle |
||||
*/ |
||||
ORACLE("oracle", "oracle.jdbc.driver.OracleDriver"), |
||||
|
||||
/** |
||||
* Postgresql |
||||
*/ |
||||
POSTGRESQL("postgresql", "org.postgresql.Driver"), |
||||
|
||||
/** |
||||
* sqlite |
||||
*/ |
||||
SQLITE("sqlite", "org.sqlite.JDBC"); |
||||
|
||||
/** |
||||
* 类型 |
||||
*/ |
||||
private final String type; |
||||
|
||||
/** |
||||
* 驱动 |
||||
*/ |
||||
private final String driverClassName; |
||||
|
||||
} |
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
package com.cloud.kicc.common.data.handler; |
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; |
||||
import com.cloud.kicc.common.data.entity.CasUser; |
||||
import org.apache.ibatis.reflection.MetaObject; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
|
||||
import java.time.LocalDateTime; |
||||
import java.util.Optional; |
||||
|
||||
/** |
||||
*<p> |
||||
* 公共字段自动填充 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
public class BaseMetaObjectHandler implements MetaObjectHandler { |
||||
|
||||
@Override |
||||
public void insertFill(MetaObject metaObject) { |
||||
Object createTime = getFieldValByName("createTime", metaObject); |
||||
if (createTime == null) { |
||||
setFieldValByName("createTime", LocalDateTime.now(), metaObject); |
||||
} |
||||
|
||||
if (Optional.ofNullable(getUser()).isPresent()) { |
||||
Object createById = getFieldValByName("createById", metaObject); |
||||
if (createById == null) { |
||||
setFieldValByName("createById", getUser().getId(), metaObject); |
||||
} |
||||
|
||||
Object createByName = getFieldValByName("createByName", metaObject); |
||||
if (createByName == null) { |
||||
setFieldValByName("createByName", getUser().getUsername(), metaObject); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void updateFill(MetaObject metaObject) { |
||||
Object fieldValue = getFieldValByName("updateTime", metaObject); |
||||
if (fieldValue == null) { |
||||
setFieldValByName("updateTime", LocalDateTime.now(), metaObject); |
||||
} |
||||
|
||||
if (Optional.ofNullable(getUser()).isPresent()) { |
||||
Object updateById = getFieldValByName("updateById", metaObject); |
||||
if (updateById == null) { |
||||
setFieldValByName("updateById", getUser().getId(), metaObject); |
||||
} |
||||
|
||||
Object updateByName = getFieldValByName("updateByName", metaObject); |
||||
if (updateByName == null) { |
||||
setFieldValByName("updateByName", getUser().getUsername(), metaObject); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取用户 |
||||
*/ |
||||
protected CasUser getUser() { |
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
||||
if (Optional.ofNullable(authentication).isPresent()) { |
||||
Object principal = authentication.getPrincipal(); |
||||
if (principal instanceof CasUser) { |
||||
return (CasUser) principal; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
} |
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
package com.cloud.kicc.common.data.handler; |
||||
|
||||
import cn.hutool.core.util.ObjectUtil; |
||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; |
||||
import com.cloud.kicc.common.data.entity.CasUser; |
||||
import com.cloud.kicc.common.data.override.TenantLikeExpression; |
||||
import com.cloud.kicc.common.data.properties.TenantProperties; |
||||
import net.sf.jsqlparser.expression.Expression; |
||||
import net.sf.jsqlparser.schema.Column; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
|
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
|
||||
/** |
||||
*<p> |
||||
* 多租户拦截处理 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/4/2 |
||||
*/ |
||||
public class KiccTenantLineHandler implements TenantLineHandler { |
||||
|
||||
private TenantProperties tenantProperties; |
||||
|
||||
public KiccTenantLineHandler(TenantProperties tenantProperties) { |
||||
this.tenantProperties = tenantProperties; |
||||
} |
||||
|
||||
/** |
||||
* 默认为tenant_id字段,尽量不要去改动,因为会牵扯到实体类中的TenantId字段 |
||||
* @return String: 表中的多租户字段 |
||||
*/ |
||||
@Override |
||||
public String getTenantIdColumn() { |
||||
return TenantLineHandler.super.getTenantIdColumn(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean ignoreTable(String tableName) { |
||||
return tenantProperties.getExclusionTable().contains(tableName); |
||||
} |
||||
|
||||
/** |
||||
* 新增数据字段中存在多租户字段是否忽略此多租户字段拼接新增 |
||||
* @Param columns: 当前新增表所有字段 |
||||
* @Param tenantIdColumn: 多租户字段 |
||||
* @return boolean: 是否忽略此多租户字段拼接新增 |
||||
*/ |
||||
@Override |
||||
public boolean ignoreInsert(List<Column> columns, String tenantIdColumn) { |
||||
return TenantLineHandler.super.ignoreInsert(columns, tenantIdColumn); |
||||
} |
||||
|
||||
@Override |
||||
public Expression getTenantId() { |
||||
// 返回当前用户所属的多租户ID进行条件拼接
|
||||
return ObjectUtil.isNotEmpty(getUser()) ? new TenantLikeExpression(getUser().getTenantId()): null; |
||||
} |
||||
|
||||
/** |
||||
* 获取用户 |
||||
*/ |
||||
protected CasUser getUser() { |
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
||||
if (Optional.ofNullable(authentication).isPresent()) { |
||||
Object principal = authentication.getPrincipal(); |
||||
if (principal instanceof CasUser) { |
||||
return (CasUser) principal; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
package com.cloud.kicc.common.data.override; |
||||
|
||||
import net.sf.jsqlparser.expression.Expression; |
||||
import net.sf.jsqlparser.expression.ExpressionVisitor; |
||||
import net.sf.jsqlparser.parser.ASTNodeAccessImpl; |
||||
|
||||
import java.util.Objects; |
||||
|
||||
/** |
||||
*<p> |
||||
* 重写StringValue,支持多租户like拼接查询 |
||||
* 由于内部StringExpression会拼接默认会加'' |
||||
* 而多租户like条件经过处理不许需要加'',如果加上会导致数据查不出 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/5/11 |
||||
*/ |
||||
public class TenantLikeExpression extends ASTNodeAccessImpl implements Expression { |
||||
|
||||
private String value = ""; |
||||
|
||||
public TenantLikeExpression() { |
||||
} |
||||
|
||||
public TenantLikeExpression(String escapedValue) { |
||||
this.value = escapedValue; |
||||
} |
||||
|
||||
public String getValue() { |
||||
return this.value; |
||||
} |
||||
|
||||
|
||||
public void setValue(String string) { |
||||
this.value = string; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public void accept(ExpressionVisitor expressionVisitor) { |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return this.value; |
||||
} |
||||
|
||||
public TenantLikeExpression withValue(String value) { |
||||
this.setValue(value); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object o) { |
||||
if (this == o) { |
||||
return true; |
||||
} else if (o != null && this.getClass() == o.getClass()) { |
||||
TenantLikeExpression that = (TenantLikeExpression)o; |
||||
return Objects.equals(this.value, that.value); |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return Objects.hash(new Object[]{this.value}); |
||||
} |
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
package com.cloud.kicc.common.data.plugins; |
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage; |
||||
import com.baomidou.mybatisplus.core.toolkit.ParameterUtils; |
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; |
||||
import lombok.Data; |
||||
import lombok.NoArgsConstructor; |
||||
import org.apache.ibatis.executor.Executor; |
||||
import org.apache.ibatis.mapping.BoundSql; |
||||
import org.apache.ibatis.mapping.MappedStatement; |
||||
import org.apache.ibatis.session.ResultHandler; |
||||
import org.apache.ibatis.session.RowBounds; |
||||
|
||||
import java.sql.SQLException; |
||||
|
||||
/** |
||||
*<p> |
||||
* 分页拦截器 |
||||
* 重构分页插件, 当 size 小于 0 时, 直接设置为 0, 防止错误查询全表 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
public class KiccPaginationInnerInterceptor extends PaginationInnerInterceptor { |
||||
|
||||
@Override |
||||
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, |
||||
ResultHandler resultHandler, BoundSql boundSql) throws SQLException { |
||||
IPage<?> page = ParameterUtils.findPage(parameter).orElse(null); |
||||
// size 小于 0 直接设置为 0 , 即不查询任何数据
|
||||
if (null != page && page.getSize() < 0) { |
||||
page.setSize(0); |
||||
} |
||||
super.beforeQuery(executor, ms, page, rowBounds, resultHandler, boundSql); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,209 @@
@@ -0,0 +1,209 @@
|
||||
package com.cloud.kicc.common.data.plugins; |
||||
|
||||
import cn.hutool.core.util.ObjectUtil; |
||||
import cn.hutool.core.util.StrUtil; |
||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; |
||||
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils; |
||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; |
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; |
||||
import com.cloud.kicc.common.core.exception.CheckedException; |
||||
import com.cloud.kicc.common.data.override.TenantLikeExpression; |
||||
import lombok.NoArgsConstructor; |
||||
import net.sf.jsqlparser.expression.Expression; |
||||
import net.sf.jsqlparser.expression.Parenthesis; |
||||
import net.sf.jsqlparser.expression.StringValue; |
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression; |
||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression; |
||||
import net.sf.jsqlparser.expression.operators.relational.ExpressionList; |
||||
import net.sf.jsqlparser.expression.operators.relational.ItemsList; |
||||
import net.sf.jsqlparser.expression.operators.relational.LikeExpression; |
||||
import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList; |
||||
import net.sf.jsqlparser.schema.Column; |
||||
import net.sf.jsqlparser.schema.Table; |
||||
import net.sf.jsqlparser.statement.delete.Delete; |
||||
import net.sf.jsqlparser.statement.insert.Insert; |
||||
import net.sf.jsqlparser.statement.select.Select; |
||||
import net.sf.jsqlparser.statement.update.Update; |
||||
import org.apache.ibatis.executor.Executor; |
||||
import org.apache.ibatis.executor.statement.StatementHandler; |
||||
import org.apache.ibatis.mapping.BoundSql; |
||||
import org.apache.ibatis.mapping.MappedStatement; |
||||
import org.apache.ibatis.session.ResultHandler; |
||||
import org.apache.ibatis.session.RowBounds; |
||||
|
||||
import java.sql.Connection; |
||||
import java.sql.SQLException; |
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
|
||||
/** |
||||
*<p> |
||||
* 租户线路内部拦截器 |
||||
* 重构租户线路内部拦截器,支持查询多个多租户ID,还支持查询多个租户的共享数据,默认只支持一个多租户id查询 |
||||
* 支持多租户ID不存在时,查询所有租户ID数据 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@NoArgsConstructor |
||||
public class KiccTenantLineInnerInterceptor extends TenantLineInnerInterceptor { |
||||
|
||||
private TenantLineHandler tenantLineHandler; |
||||
|
||||
public KiccTenantLineInnerInterceptor(final TenantLineHandler tenantLineHandler) { |
||||
super(tenantLineHandler); |
||||
this.tenantLineHandler = tenantLineHandler; |
||||
} |
||||
|
||||
@Override |
||||
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { |
||||
if (ObjectUtil.isNotEmpty(tenantLineHandler.getTenantId())){ |
||||
super.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { |
||||
if (ObjectUtil.isNotEmpty(tenantLineHandler.getTenantId())) { |
||||
super.beforePrepare(sh, connection, transactionTimeout); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected void processInsert(Insert insert, int index, String sql, Object obj) { |
||||
if (!this.tenantLineHandler.ignoreTable(insert.getTable().getName())) { |
||||
List<Column> columns = insert.getColumns(); |
||||
if (!CollectionUtils.isEmpty(columns)) { |
||||
String tenantIdColumn = this.tenantLineHandler.getTenantIdColumn(); |
||||
if (!this.tenantLineHandler.ignoreInsert(columns, tenantIdColumn)) { |
||||
columns.add(new Column(tenantIdColumn)); |
||||
|
||||
List<Expression> duplicateUpdateColumns = insert.getDuplicateUpdateExpressionList(); |
||||
if (CollectionUtils.isNotEmpty(duplicateUpdateColumns)) { |
||||
// 替换likeExpression支持查询多个租户ID的条件,包括查询数据对应多个多租户ID的数据
|
||||
List<String> tenantIds = StrUtil.split(this.tenantLineHandler.getTenantId().toString(), ","); |
||||
StringBuilder statementBuilder = new StringBuilder(); |
||||
tenantIds.forEach(tenantId -> { |
||||
LikeExpression likeExpression = new LikeExpression(); |
||||
likeExpression.setLeftExpression(new Column(tenantIdColumn)); |
||||
likeExpression.setRightExpression(new StringValue("%" + tenantId + "%")); |
||||
statementBuilder.append(likeExpression + " OR "); |
||||
}); |
||||
if (statementBuilder.length() == 0) { |
||||
throw new CheckedException("当前用户没有分配租户"); |
||||
} |
||||
statementBuilder.delete(statementBuilder.length()-4, statementBuilder.length()); |
||||
TenantLikeExpression tenantLikeExpression = new TenantLikeExpression(statementBuilder.toString()); |
||||
Parenthesis parenthesis = new Parenthesis(tenantLikeExpression); |
||||
duplicateUpdateColumns.add(parenthesis); |
||||
} |
||||
|
||||
Select select = insert.getSelect(); |
||||
if (select != null) { |
||||
this.processInsertSelect(select.getSelectBody()); |
||||
} else { |
||||
if (insert.getItemsList() == null) { |
||||
throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId", new Object[0]); |
||||
} |
||||
|
||||
ItemsList itemsList = insert.getItemsList(); |
||||
if (itemsList instanceof MultiExpressionList) { |
||||
((MultiExpressionList)itemsList).getExpressionLists().forEach((el) -> { |
||||
el.getExpressions().add(new StringValue(this.tenantLineHandler.getTenantId().toString())); |
||||
}); |
||||
} else { |
||||
((ExpressionList)itemsList).getExpressions().add(new StringValue(this.tenantLineHandler.getTenantId().toString())); |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected void processUpdate(Update update, int index, String sql, Object obj) { |
||||
Table table = update.getTable(); |
||||
if (!this.tenantLineHandler.ignoreTable(table.getName())) { |
||||
update.setWhere(this.andLikeExpression(table, update.getWhere())); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected void processDelete(Delete delete, int index, String sql, Object obj) { |
||||
if (!this.tenantLineHandler.ignoreTable(delete.getTable().getName())) { |
||||
delete.setWhere(this.andLikeExpression(delete.getTable(), delete.getWhere())); |
||||
} |
||||
} |
||||
|
||||
/** 重写andExpression表达式,支持like查询多个参数 */ |
||||
protected Expression andLikeExpression(Table table, Expression where) { |
||||
// 替换likeExpression支持查询多个租户ID的条件,包括查询数据对应多个多租户ID的数据
|
||||
List<String> tenantIds = StrUtil.split(this.tenantLineHandler.getTenantId().toString(), ","); |
||||
StringBuilder statementBuilder = new StringBuilder(); |
||||
tenantIds.forEach(tenantId -> { |
||||
LikeExpression likeExpression = new LikeExpression(); |
||||
likeExpression.setLeftExpression(this.getAliasColumn(table)); |
||||
likeExpression.setRightExpression(new StringValue("%" + tenantId + "%")); |
||||
statementBuilder.append(likeExpression + " OR "); |
||||
}); |
||||
if (statementBuilder.length() == 0) { |
||||
throw new CheckedException("当前用户没有分配租户"); |
||||
} |
||||
statementBuilder.delete(statementBuilder.length()-4, statementBuilder.length()); |
||||
TenantLikeExpression tenantLikeExpression = new TenantLikeExpression(statementBuilder.toString()); |
||||
Parenthesis parenthesis = new Parenthesis(tenantLikeExpression); |
||||
if (null != where) { |
||||
return where instanceof OrExpression ? new AndExpression(parenthesis, new Parenthesis(where)) : new AndExpression(parenthesis, where); |
||||
} else { |
||||
return parenthesis; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected Expression builderExpression(Expression currentExpression, List<Table> tables) { |
||||
if (CollectionUtils.isEmpty(tables)) { |
||||
return currentExpression; |
||||
} else { |
||||
// 替换likeExpression支持查询多个租户ID的条件,包括查询数据对应多个多租户ID的数据
|
||||
List<Parenthesis> likeParenthesisExpressions = tables.stream() |
||||
.filter(x -> !this.tenantLineHandler.ignoreTable(x.getName())) |
||||
.map(item -> { |
||||
List<String> tenantIds = StrUtil.split(this.tenantLineHandler.getTenantId().toString(), ","); |
||||
StringBuilder statementBuilder = new StringBuilder(); |
||||
tenantIds.forEach(tenantId -> { |
||||
LikeExpression likeExpression = new LikeExpression(); |
||||
likeExpression.setLeftExpression(this.getAliasColumn(item)); |
||||
likeExpression.setRightExpression(new StringValue("%" + tenantId + "%")); |
||||
statementBuilder.append(likeExpression + " OR "); |
||||
}); |
||||
if (statementBuilder.length() == 0) { |
||||
throw new CheckedException("当前用户没有分配租户"); |
||||
} |
||||
statementBuilder.delete(statementBuilder.length()-4, statementBuilder.length()); |
||||
TenantLikeExpression tenantLikeExpression = new TenantLikeExpression(statementBuilder.toString()); |
||||
Parenthesis parenthesis = new Parenthesis(tenantLikeExpression); |
||||
return parenthesis; |
||||
}).collect(Collectors.toList()); |
||||
|
||||
if (CollectionUtils.isEmpty(likeParenthesisExpressions)) { |
||||
return currentExpression; |
||||
} else { |
||||
Expression injectExpression = likeParenthesisExpressions.get(0); |
||||
if (likeParenthesisExpressions.size() > 1) { |
||||
for(int i = 1; i < likeParenthesisExpressions.size(); ++i) { |
||||
injectExpression = new AndExpression(injectExpression, likeParenthesisExpressions.get(i)); |
||||
} |
||||
} |
||||
|
||||
if (currentExpression == null) { |
||||
return injectExpression; |
||||
} else { |
||||
return currentExpression instanceof OrExpression ? new AndExpression(new Parenthesis(currentExpression), injectExpression) : new AndExpression(currentExpression, injectExpression); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
package com.cloud.kicc.common.data.properties; |
||||
|
||||
import lombok.Data; |
||||
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
*<p> |
||||
* 多租户配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/4/2+ |
||||
*/ |
||||
@Data |
||||
@ConfigurationProperties(prefix = "tenant") |
||||
public class TenantProperties { |
||||
|
||||
private Boolean enableInsert; |
||||
|
||||
private List<String> exclusionTable; |
||||
|
||||
} |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration= |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
tenant: |
||||
# 需要排除的多租户的表 |
||||
exclusionTable: |
||||
- sys_tenant |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-tool</artifactId> |
||||
<version>1.0.0</version> |
||||
</parent> |
||||
|
||||
<artifactId>kicc-common-datasource</artifactId> |
||||
<packaging>jar</packaging> |
||||
|
||||
<description>kicc 动态切换数据源组件</description> |
||||
|
||||
<!--考虑这个作为一个单模块使用,后续引入依赖需要注意低耦合--> |
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-data</artifactId> |
||||
</dependency> |
||||
<!--mybatis-dynamic-datasource--> |
||||
<dependency> |
||||
<groupId>com.baomidou</groupId> |
||||
<artifactId>dynamic-datasource-spring-boot-starter</artifactId> |
||||
<version>${dynamic-ds.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.apache.commons</groupId> |
||||
<artifactId>commons-collections4</artifactId> |
||||
<version>${apache.collections4.version}</version> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
package com.cloud.kicc.common.datasource; |
||||
|
||||
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; |
||||
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator; |
||||
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider; |
||||
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; |
||||
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; |
||||
import com.cloud.kicc.common.datasource.dynamic.DynamicDataSourceJdbcProvider; |
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
|
||||
import javax.sql.DataSource; |
||||
|
||||
/** |
||||
*<p> |
||||
* 动态数据源切换配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/19 |
||||
*/ |
||||
@Configuration(proxyBeanMethods = false) |
||||
public class DynamicDataSourceConfiguration { |
||||
|
||||
@Bean |
||||
public DynamicDataSourceProvider dynamicDataSourceProvider(DataSourceProperties dataSourceProperties, DynamicDataSourceProperties dynamicDataSourceProperties) { |
||||
String driverClassName = dataSourceProperties.getDriverClassName(); |
||||
String url = dataSourceProperties.getUrl(); |
||||
String username = dataSourceProperties.getUsername(); |
||||
String password = dataSourceProperties.getPassword(); |
||||
DataSourceProperty master = dynamicDataSourceProperties.getDatasource().get(dynamicDataSourceProperties.getPrimary()); |
||||
if (master != null) { |
||||
driverClassName = master.getDriverClassName(); |
||||
url = master.getUrl(); |
||||
username = master.getUsername(); |
||||
password = master.getPassword(); |
||||
} |
||||
return new DynamicDataSourceJdbcProvider(dynamicDataSourceProperties, driverClassName, url, username, password); |
||||
} |
||||
|
||||
@Bean |
||||
public DataSource dataSource(DynamicDataSourceProperties dynamicDataSourceProperties) { |
||||
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(); |
||||
dataSource.setPrimary(dynamicDataSourceProperties.getPrimary()); |
||||
dataSource.setStrict(dynamicDataSourceProperties.getStrict()); |
||||
dataSource.setStrategy(dynamicDataSourceProperties.getStrategy()); |
||||
dataSource.setP6spy(dynamicDataSourceProperties.getP6spy()); |
||||
dataSource.setSeata(dynamicDataSourceProperties.getSeata()); |
||||
return dataSource; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
package com.cloud.kicc.common.datasource.annotation; |
||||
|
||||
import com.cloud.kicc.common.datasource.DynamicDataSourceConfiguration; |
||||
import org.springframework.context.annotation.Import; |
||||
|
||||
import java.lang.annotation.*; |
||||
|
||||
/** |
||||
*<p> |
||||
* 开启动态数据源 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/19 |
||||
*/ |
||||
@Target({ ElementType.TYPE }) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Documented |
||||
@Inherited |
||||
@Import(DynamicDataSourceConfiguration.class) |
||||
public @interface EnableDynamicDataSource { |
||||
|
||||
} |
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
package com.cloud.kicc.common.datasource.dynamic; |
||||
|
||||
import lombok.Data; |
||||
import lombok.EqualsAndHashCode; |
||||
import lombok.experimental.Accessors; |
||||
|
||||
/** |
||||
*<p> |
||||
* 动态数据源 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Since: 2023/7/3 |
||||
*/ |
||||
@Data |
||||
@Accessors(chain = true) |
||||
@EqualsAndHashCode(callSuper = false) |
||||
public class DynamicDataSource { |
||||
|
||||
/** |
||||
* 数据源ID |
||||
*/ |
||||
private String id; |
||||
|
||||
/** |
||||
* 驱动类 |
||||
*/ |
||||
private String driverClass; |
||||
|
||||
/** |
||||
* 数据库链接 |
||||
*/ |
||||
private String url; |
||||
|
||||
/** |
||||
* 数据库账号名 |
||||
*/ |
||||
private String username; |
||||
|
||||
/** |
||||
* 数据库密码 |
||||
*/ |
||||
private String password; |
||||
|
||||
} |
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
package com.cloud.kicc.common.datasource.dynamic; |
||||
|
||||
import cn.hutool.core.util.StrUtil; |
||||
import com.baomidou.dynamic.datasource.provider.AbstractJdbcDataSourceProvider; |
||||
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; |
||||
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; |
||||
import com.cloud.kicc.common.datasource.support.DynamicDataSourceConstant; |
||||
import com.cloud.kicc.common.datasource.util.ConnUtil; |
||||
|
||||
import java.sql.ResultSet; |
||||
import java.sql.SQLException; |
||||
import java.sql.Statement; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
*<p> |
||||
* 动态数据源初始加载 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Since: 2023/7/3 |
||||
*/ |
||||
public class DynamicDataSourceJdbcProvider extends AbstractJdbcDataSourceProvider { |
||||
|
||||
private final String driverClassName; |
||||
private final String url; |
||||
private final String username; |
||||
private final String password; |
||||
private final DynamicDataSourceProperties dynamicDataSourceProperties; |
||||
|
||||
public DynamicDataSourceJdbcProvider(DynamicDataSourceProperties dynamicDataSourceProperties, |
||||
String driverClassName, |
||||
String url, |
||||
String username, |
||||
String password) { |
||||
super(driverClassName, url, username, password); |
||||
this.dynamicDataSourceProperties = dynamicDataSourceProperties; |
||||
this.driverClassName = driverClassName; |
||||
this.url = url; |
||||
this.username = username; |
||||
this.password = password; |
||||
} |
||||
|
||||
@Override |
||||
protected Map<String, DataSourceProperty> executeStmt(Statement statement) throws SQLException { |
||||
// 构建数据源集合
|
||||
Map<String, DataSourceProperty> map = new HashMap<>(16); |
||||
// 构建主数据源
|
||||
DataSourceProperty masterProperty = new DataSourceProperty(); |
||||
masterProperty.setDriverClassName(driverClassName); |
||||
masterProperty.setUrl(url); |
||||
masterProperty.setUsername(username); |
||||
masterProperty.setPassword(password); |
||||
map.put(dynamicDataSourceProperties.getPrimary(), masterProperty); |
||||
// 构建yml数据源
|
||||
Map<String, DataSourceProperty> datasource = dynamicDataSourceProperties.getDatasource(); |
||||
if (!datasource.isEmpty()) { |
||||
datasource.remove(dynamicDataSourceProperties.getPrimary()); |
||||
map.putAll(datasource); |
||||
} |
||||
// 构建动态数据源
|
||||
ResultSet rs = statement.executeQuery(DynamicDataSourceConstant.DYNAMIC_DATASOURCE_GROUP_STATEMENT); |
||||
while (rs.next()) { |
||||
String id = rs.getString("id"); |
||||
String name = rs.getString("name"); |
||||
String driver = rs.getString("driverClass"); |
||||
String url = rs.getString("url"); |
||||
String username = rs.getString("username"); |
||||
String password = rs.getString("password"); |
||||
try { |
||||
if (StrUtil.isAllNotBlank(id, driver, url, username, password)) { |
||||
// 测试链接是否生效
|
||||
Boolean result = ConnUtil.dbTest(driver, url, username, password); |
||||
if (result) { |
||||
DataSourceProperty jdbcProperty = new DataSourceProperty(); |
||||
// 设置SQL链接
|
||||
jdbcProperty.setDriverClassName(driver); |
||||
jdbcProperty.setUrl(url); |
||||
jdbcProperty.setUsername(username); |
||||
jdbcProperty.setPassword(password); |
||||
map.put(name, jdbcProperty); |
||||
} |
||||
} |
||||
} catch (Exception e) { |
||||
System.err.printf("数据源:"+ name + "初始化失败!"); |
||||
} |
||||
} |
||||
return map; |
||||
} |
||||
} |
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
package com.cloud.kicc.common.datasource.support; |
||||
|
||||
/** |
||||
*<p> |
||||
* 数据源常量 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Since: 2023/7/3 |
||||
*/ |
||||
public interface DynamicDataSourceConstant { |
||||
|
||||
/** |
||||
* 数据源查询基础 |
||||
*/ |
||||
String DYNAMIC_DATASOURCE_BASE_STATEMENT = "SELECT id, name, driver_class as driverClass, url, username, password FROM sys_datasource"; |
||||
|
||||
/** |
||||
* 数据源查询SQL |
||||
*/ |
||||
String DYNAMIC_DATASOURCE_SINGLE_STATEMENT = DYNAMIC_DATASOURCE_BASE_STATEMENT + " WHERE del_flag = 0 AND id = ?"; |
||||
|
||||
/** |
||||
* 数据源查询SQL |
||||
*/ |
||||
String DYNAMIC_DATASOURCE_GROUP_STATEMENT = DYNAMIC_DATASOURCE_BASE_STATEMENT + " WHERE del_flag = 0"; |
||||
|
||||
/** |
||||
* 数据源错误提示 |
||||
*/ |
||||
String DYNAMIC_DATASOURCE_NOT_FOUND = "数据源信息有误,数据加载失败"; |
||||
|
||||
/** |
||||
* oracle驱动类 |
||||
*/ |
||||
String ORACLE_DRIVER_CLASS = "oracle.jdbc.OracleDriver"; |
||||
|
||||
/** |
||||
* oracle校验 |
||||
*/ |
||||
String ORACLE_VALIDATE_STATEMENT = "select 1 from dual"; |
||||
|
||||
/** |
||||
* 通用校验 |
||||
*/ |
||||
String COMMON_VALIDATE_STATEMENT = "select 1"; |
||||
|
||||
} |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
package com.cloud.kicc.common.datasource.util; |
||||
|
||||
import lombok.SneakyThrows; |
||||
|
||||
import java.sql.Connection; |
||||
import java.sql.DriverManager; |
||||
import java.sql.SQLException; |
||||
|
||||
/** |
||||
*<p> |
||||
* 数据库工具类 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Since: 2023/7/3 |
||||
*/ |
||||
public class ConnUtil { |
||||
|
||||
/** |
||||
* 测试数据库链接 |
||||
*/ |
||||
@SneakyThrows |
||||
public static Boolean dbTest(String driverClass, String url, String username, String password) { |
||||
Connection conn = null; |
||||
try { |
||||
//测试驱动类
|
||||
Class.forName(driverClass); |
||||
//创建连接
|
||||
conn = DriverManager.getConnection(url, username, password); |
||||
conn.setAutoCommit(Boolean.FALSE); |
||||
return true; |
||||
} finally { |
||||
//关闭连接
|
||||
dbClose(conn); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 关闭数据库链接 |
||||
*/ |
||||
private static void dbClose(Connection conn) { |
||||
try { |
||||
//关闭数据源连接
|
||||
if (conn != null) { |
||||
conn.close(); |
||||
} |
||||
} catch (Exception ex) { |
||||
ex.printStackTrace(); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,112 @@
@@ -0,0 +1,112 @@
|
||||
package com.cloud.kicc.common.datasource.util; |
||||
|
||||
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; |
||||
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator; |
||||
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; |
||||
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; |
||||
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; |
||||
import com.cloud.kicc.common.datasource.dynamic.DynamicDataSource; |
||||
import lombok.Setter; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.commons.collections4.map.LRUMap; |
||||
import org.springframework.beans.BeanUtils; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import javax.sql.DataSource; |
||||
import java.sql.Connection; |
||||
import java.sql.SQLException; |
||||
|
||||
|
||||
/** |
||||
*<p> |
||||
* 动态数据源核心处理工具 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Since: 2023/7/3 |
||||
*/ |
||||
@Setter |
||||
@Slf4j |
||||
@Component |
||||
public class DynamicDataSourceUtil { |
||||
|
||||
public static DynamicRoutingDataSource dynamicRoutingDataSource; |
||||
public static DynamicDataSourceProperties dynamicDataSourceProperties; |
||||
private static DefaultDataSourceCreator defaultDataSourceCreator; |
||||
private static int MAX_DATASOURCE_COUNT = 300; |
||||
// 最多保存三百个数据源,按使用率淘汰
|
||||
private static final LRUMap<String, DynamicDataSource> linksProperties = new LRUMap(MAX_DATASOURCE_COUNT); |
||||
|
||||
public DynamicDataSourceUtil(DataSource dynamicRoutingDataSource, |
||||
DynamicDataSourceProperties dynamicDataSourceProperties, |
||||
DefaultDataSourceCreator defaultDataSourceCreator) { |
||||
DynamicDataSourceUtil.dynamicRoutingDataSource = (DynamicRoutingDataSource) dynamicRoutingDataSource; |
||||
DynamicDataSourceUtil.dynamicDataSourceProperties = dynamicDataSourceProperties; |
||||
DynamicDataSourceUtil.defaultDataSourceCreator = defaultDataSourceCreator; |
||||
} |
||||
|
||||
/** |
||||
* 创建并切换至远程数据源 |
||||
* @param dynamicDataSource 切换数据源 |
||||
*/ |
||||
public static void switchToDataSource(DynamicDataSource dynamicDataSource) { |
||||
String dbKey = dynamicDataSource.getId(); |
||||
String removeKey = null; |
||||
boolean insert = true; |
||||
if (dynamicRoutingDataSource.getDataSources().containsKey(dynamicDataSource.getId())) { |
||||
synchronized (linksProperties) { |
||||
if (linksProperties.get(dbKey).equals(dynamicDataSource)) { |
||||
insert = false; |
||||
} |
||||
} |
||||
} |
||||
if (insert) { |
||||
// 创建数据源配置
|
||||
DataSourceProperty dataSourceProperty = new DataSourceProperty(); |
||||
// 拷贝数据源配置
|
||||
BeanUtils.copyProperties(dynamicDataSource, dataSourceProperty); |
||||
// 创建动态数据源
|
||||
DataSource dataSource = defaultDataSourceCreator.createDataSource(dataSourceProperty); |
||||
// 添加最新数据源
|
||||
dynamicRoutingDataSource.addDataSource(dbKey, dataSource); |
||||
synchronized (linksProperties) { |
||||
if (linksProperties.size() == MAX_DATASOURCE_COUNT) { |
||||
removeKey = linksProperties.firstKey(); |
||||
} |
||||
linksProperties.put(dbKey, dynamicDataSource); |
||||
} |
||||
} |
||||
// 切换数据源
|
||||
DynamicDataSourceContextHolder.push(dbKey); |
||||
if (removeKey != null) { |
||||
try { |
||||
dynamicRoutingDataSource.removeDataSource(removeKey); |
||||
} catch (Exception e) { |
||||
log.error("移除数据源失败:{}", e.getMessage()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 移除当前设置的远程数据源,清除上次清除之后切换的所有数据源 |
||||
* 需要先调用 switchToDataSource切换数据源 |
||||
*/ |
||||
public static void clearSwitchDataSource() { |
||||
DynamicDataSourceContextHolder.poll(); |
||||
} |
||||
|
||||
/** |
||||
* 获取当前数据源的数据链接(切库后的) |
||||
* 用完之后一定要关闭 |
||||
* @return |
||||
* @throws SQLException |
||||
*/ |
||||
public static Connection getCurrentConnection() throws SQLException { |
||||
return dynamicRoutingDataSource.getConnection(); |
||||
} |
||||
|
||||
public static boolean containsLink(String key) { |
||||
return linksProperties.containsKey(key); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
||||
com.cloud.kicc.common.datasource.util.DynamicDataSourceUtil |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-tool</artifactId> |
||||
<version>1.0.0</version> |
||||
</parent> |
||||
|
||||
<artifactId>kicc-common-feign</artifactId> |
||||
<packaging>jar</packaging> |
||||
|
||||
<description>feign-sentinel服务降级熔断、限流组件</description> |
||||
|
||||
<!--考虑这个作为一个单模块使用,目前依赖了工具类核心包,后续引入依赖需要注意低耦合--> |
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-core</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.alibaba.cloud</groupId> |
||||
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> |
||||
</dependency> |
||||
<!--feign 依赖--> |
||||
<dependency> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-starter-openfeign</artifactId> |
||||
</dependency> |
||||
<!-- okhttp 扩展 --> |
||||
<dependency> |
||||
<groupId>io.github.openfeign</groupId> |
||||
<artifactId>feign-okhttp</artifactId> |
||||
</dependency> |
||||
<!-- LB 扩展 --> |
||||
<dependency> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId> |
||||
</dependency> |
||||
<!--caffeine 替换LB 默认缓存实现--> |
||||
<dependency> |
||||
<groupId>com.github.ben-manes.caffeine</groupId> |
||||
<artifactId>caffeine</artifactId> |
||||
</dependency> |
||||
<!--oauth server 依赖--> |
||||
<dependency> |
||||
<groupId>org.springframework.security.oauth.boot</groupId> |
||||
<artifactId>spring-security-oauth2-autoconfigure</artifactId> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
package com.cloud.kicc.common.feign; |
||||
|
||||
import com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration; |
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; |
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; |
||||
import com.cloud.kicc.common.feign.sentinel.ext.KiccSentinelFeign; |
||||
import com.cloud.kicc.common.feign.sentinel.handle.KiccUrlBlockHandler; |
||||
import com.cloud.kicc.common.feign.sentinel.parser.KiccHeaderRequestOriginParser; |
||||
import feign.Feign; |
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.annotation.Scope; |
||||
|
||||
/** |
||||
*<p> |
||||
* sentinel 配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@Configuration(proxyBeanMethods = false) |
||||
@AutoConfigureBefore(SentinelFeignAutoConfiguration.class) |
||||
public class KiccFeignAutoConfiguration { |
||||
|
||||
@Bean |
||||
@Scope("prototype") |
||||
@ConditionalOnMissingBean |
||||
@ConditionalOnProperty(name = "feign.sentinel.enabled") |
||||
public Feign.Builder feignSentinelBuilder() { |
||||
return KiccSentinelFeign.builder(); |
||||
} |
||||
|
||||
@Bean |
||||
@ConditionalOnMissingBean |
||||
public BlockExceptionHandler blockExceptionHandler() { |
||||
return new KiccUrlBlockHandler(); |
||||
} |
||||
|
||||
@Bean |
||||
@ConditionalOnMissingBean |
||||
public RequestOriginParser requestOriginParser() { |
||||
return new KiccHeaderRequestOriginParser(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
package com.cloud.kicc.common.feign.annotation; |
||||
|
||||
import com.cloud.kicc.common.feign.config.FeignErrorDecoder; |
||||
import com.cloud.kicc.common.feign.config.KiccFeignClientConfiguration; |
||||
import org.springframework.cloud.openfeign.EnableFeignClients; |
||||
import org.springframework.cloud.openfeign.FeignClientsConfiguration; |
||||
import org.springframework.cloud.openfeign.KiccFeignClientsRegistrar; |
||||
import org.springframework.context.annotation.Import; |
||||
|
||||
import java.lang.annotation.*; |
||||
|
||||
/** |
||||
*<p> |
||||
* 扩展Feign请求接口支持自动熔断降级 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@Target(ElementType.TYPE) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Documented |
||||
@EnableFeignClients |
||||
@Import({ KiccFeignClientsRegistrar.class, KiccFeignClientConfiguration.class }) |
||||
public @interface EnableKiccFeignClients { |
||||
|
||||
/** |
||||
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation |
||||
* declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of |
||||
* {@code @ComponentScan(basePackages="org.my.pkg")}. |
||||
* @return the array of 'basePackages'. |
||||
*/ |
||||
String[] value() default {}; |
||||
|
||||
/** |
||||
* Base packages to scan for annotated components. |
||||
* <p> |
||||
* {@link #value()} is an alias for (and mutually exclusive with) this attribute. |
||||
* <p> |
||||
* Use {@link #basePackageClasses()} for a type-safe alternative to String-based |
||||
* package names. |
||||
* @return the array of 'basePackages'. |
||||
*/ |
||||
String[] basePackages() default { "com.cloud.kicc" }; |
||||
|
||||
/** |
||||
* Type-safe alternative to {@link #basePackages()} for specifying the packages to |
||||
* scan for annotated components. The package of each class specified will be scanned. |
||||
* <p> |
||||
* Consider creating a special no-op marker class or interface in each package that |
||||
* serves no purpose other than being referenced by this attribute. |
||||
* @return the array of 'basePackageClasses'. |
||||
*/ |
||||
Class<?>[] basePackageClasses() default {}; |
||||
|
||||
/** |
||||
* A custom <code>@Configuration</code> for all feign clients. Can contain override |
||||
* <code>@Bean</code> definition for the pieces that make up the client, for instance |
||||
* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. |
||||
* |
||||
* @see FeignClientsConfiguration for the defaults |
||||
*/ |
||||
Class<?>[] defaultConfiguration() default { FeignErrorDecoder.class }; |
||||
|
||||
/** |
||||
* List of classes annotated with @FeignClient. If not empty, disables classpath |
||||
* scanning. |
||||
* @return |
||||
*/ |
||||
Class<?>[] clients() default {}; |
||||
|
||||
} |
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
package com.cloud.kicc.common.feign.config; |
||||
|
||||
import cn.hutool.json.JSONUtil; |
||||
import com.alibaba.fastjson.JSON; |
||||
import com.cloud.kicc.common.core.api.R; |
||||
import feign.FeignException; |
||||
import feign.Response; |
||||
import feign.RetryableException; |
||||
import feign.codec.ErrorDecoder; |
||||
import lombok.SneakyThrows; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.context.annotation.Configuration; |
||||
|
||||
import java.nio.ByteBuffer; |
||||
import java.nio.charset.StandardCharsets; |
||||
|
||||
/** |
||||
*<p> |
||||
* 自定义feign错误响应数据 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/5/16 |
||||
*/ |
||||
@Slf4j |
||||
@Configuration |
||||
public class FeignErrorDecoder extends ErrorDecoder.Default { |
||||
|
||||
@Override |
||||
@SneakyThrows |
||||
public Exception decode(String methodKey, Response response) { |
||||
Exception exception = super.decode(methodKey, response); |
||||
// 如果是RetryableException,则返回继续重试
|
||||
if (exception instanceof RetryableException) { |
||||
return exception; |
||||
} |
||||
try { |
||||
// 如果是FeignException,则对其进行处理,并抛出Exception
|
||||
if (exception instanceof FeignException && ((FeignException) exception).responseBody().isPresent()) { |
||||
ByteBuffer responseBody = ((FeignException) exception).responseBody().get(); |
||||
String bodyText = StandardCharsets.UTF_8.newDecoder().decode(responseBody.asReadOnlyBuffer()).toString(); |
||||
return new Exception(JSONUtil.isJson(bodyText) ? JSONUtil.toBean(bodyText, R.class).getMsg() : bodyText); |
||||
} |
||||
} catch (Exception ex) { |
||||
log.error(ex.getMessage(), ex); |
||||
} |
||||
return exception; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
package com.cloud.kicc.common.feign.config; |
||||
|
||||
import feign.RequestInterceptor; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
import org.springframework.cloud.commons.security.AccessTokenContextRelay; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.security.oauth2.client.OAuth2ClientContext; |
||||
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; |
||||
|
||||
/** |
||||
*<p> |
||||
* 拦截器传递 header 中oauth token,使用hystrix 的信号量模式 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@ConditionalOnProperty("security.oauth2.client.client-id") |
||||
public class KiccFeignClientConfiguration { |
||||
|
||||
@Bean |
||||
public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails resource, AccessTokenContextRelay accessTokenContextRelay) { |
||||
return new KiccFeignClientInterceptor(oAuth2ClientContext, resource, accessTokenContextRelay); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
package com.cloud.kicc.common.feign.config; |
||||
|
||||
import cn.hutool.core.collection.CollUtil; |
||||
import com.cloud.kicc.common.core.constant.SecurityConstants; |
||||
import feign.RequestTemplate; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.cloud.commons.security.AccessTokenContextRelay; |
||||
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor; |
||||
import org.springframework.security.oauth2.client.OAuth2ClientContext; |
||||
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; |
||||
|
||||
import java.util.Collection; |
||||
|
||||
/** |
||||
*<p> |
||||
* 扩展OAuth2FeignRequestInterceptor |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@Slf4j |
||||
public class KiccFeignClientInterceptor extends OAuth2FeignRequestInterceptor { |
||||
|
||||
private final OAuth2ClientContext oAuth2ClientContext; |
||||
|
||||
private final AccessTokenContextRelay accessTokenContextRelay; |
||||
|
||||
/** |
||||
* 在授权标头中使用提供的 OAuth2ClientContext 和 Bearer 令牌的默认构造函数 |
||||
* @param oAuth2ClientContext provided context |
||||
* @param resource type of resource to be accessed |
||||
* @param accessTokenContextRelay |
||||
*/ |
||||
public KiccFeignClientInterceptor(OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails resource, |
||||
AccessTokenContextRelay accessTokenContextRelay) { |
||||
super(oAuth2ClientContext, resource); |
||||
this.oAuth2ClientContext = oAuth2ClientContext; |
||||
this.accessTokenContextRelay = accessTokenContextRelay; |
||||
} |
||||
|
||||
/** |
||||
* 使用提供的名称和提取的提取物的标题创建模板 |
||||
* 1. 如果使用 非web 请求,header 区别 |
||||
* 2. 根据authentication 还原请求token |
||||
* @param template |
||||
*/ |
||||
@Override |
||||
public void apply(RequestTemplate template) { |
||||
Collection<String> fromHeader = template.headers().get(SecurityConstants.FROM); |
||||
if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) { |
||||
return; |
||||
} |
||||
// 进行token中转,传递token
|
||||
accessTokenContextRelay.copyToken(); |
||||
if (oAuth2ClientContext != null && oAuth2ClientContext.getAccessToken() != null) { |
||||
super.apply(template); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
package com.cloud.kicc.common.feign.sentinel; |
||||
|
||||
import com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration; |
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; |
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; |
||||
import com.cloud.kicc.common.feign.sentinel.ext.KiccSentinelFeign; |
||||
import com.cloud.kicc.common.feign.sentinel.ext.KiccSentinelFilterConfiguration; |
||||
import com.cloud.kicc.common.feign.sentinel.handle.KiccUrlBlockHandler; |
||||
import com.cloud.kicc.common.feign.sentinel.parser.KiccHeaderRequestOriginParser; |
||||
import feign.Feign; |
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.annotation.Import; |
||||
import org.springframework.context.annotation.Scope; |
||||
|
||||
/** |
||||
*<p> |
||||
* sentinel 配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@Configuration(proxyBeanMethods = false) |
||||
@Import(KiccSentinelFilterConfiguration.class) |
||||
@AutoConfigureBefore(SentinelFeignAutoConfiguration.class) |
||||
public class SentinelAutoConfiguration { |
||||
|
||||
@Bean |
||||
@Scope("prototype") |
||||
@ConditionalOnMissingBean |
||||
@ConditionalOnProperty(name = "feign.sentinel.enabled") |
||||
public Feign.Builder feignSentinelBuilder() { |
||||
return KiccSentinelFeign.builder(); |
||||
} |
||||
|
||||
@Bean |
||||
@ConditionalOnMissingBean |
||||
public BlockExceptionHandler blockExceptionHandler() { |
||||
return new KiccUrlBlockHandler(); |
||||
} |
||||
|
||||
@Bean |
||||
@ConditionalOnMissingBean |
||||
public RequestOriginParser requestOriginParser() { |
||||
return new KiccHeaderRequestOriginParser(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,132 @@
@@ -0,0 +1,132 @@
|
||||
package com.cloud.kicc.common.feign.sentinel.ext; |
||||
|
||||
import com.alibaba.cloud.sentinel.feign.SentinelContractHolder; |
||||
import feign.Contract; |
||||
import feign.Feign; |
||||
import feign.InvocationHandlerFactory; |
||||
import feign.Target; |
||||
import org.springframework.beans.BeansException; |
||||
import org.springframework.cloud.openfeign.FallbackFactory; |
||||
import org.springframework.cloud.openfeign.FeignClient; |
||||
import org.springframework.cloud.openfeign.FeignContext; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.ApplicationContextAware; |
||||
import org.springframework.core.annotation.AnnotationUtils; |
||||
import org.springframework.util.ReflectionUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import java.lang.reflect.Field; |
||||
import java.lang.reflect.InvocationHandler; |
||||
import java.lang.reflect.Method; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
*<p> |
||||
* 支持自动降级注入 重写 {@link com.alibaba.cloud.sentinel.feign.SentinelFeign} |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
public final class KiccSentinelFeign { |
||||
|
||||
private KiccSentinelFeign() { |
||||
|
||||
} |
||||
|
||||
public static KiccSentinelFeign.Builder builder() { |
||||
return new KiccSentinelFeign.Builder(); |
||||
} |
||||
|
||||
public static final class Builder extends Feign.Builder implements ApplicationContextAware { |
||||
|
||||
private Contract contract = new Contract.Default(); |
||||
|
||||
private ApplicationContext applicationContext; |
||||
|
||||
private FeignContext feignContext; |
||||
|
||||
@Override |
||||
public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
@Override |
||||
public KiccSentinelFeign.Builder contract(Contract contract) { |
||||
this.contract = contract; |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public Feign build() { |
||||
super.invocationHandlerFactory(new InvocationHandlerFactory() { |
||||
@Override |
||||
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) { |
||||
|
||||
// 查找 FeignClient 上的降级策略
|
||||
FeignClient feignClient = AnnotationUtils.findAnnotation(target.type(), FeignClient.class); |
||||
Class<?> fallback = feignClient.fallback(); |
||||
Class<?> fallbackFactory = feignClient.fallbackFactory(); |
||||
|
||||
// 查找 FeignClient 上的上下文唯一ID,也就是是注入的bean名称
|
||||
String beanName = feignClient.contextId(); |
||||
if (!StringUtils.hasText(beanName)) { |
||||
beanName = feignClient.name(); |
||||
} |
||||
|
||||
// 开始处理自动降级
|
||||
Object fallbackInstance; |
||||
FallbackFactory<?> fallbackFactoryInstance; |
||||
if (void.class != fallback) { |
||||
fallbackInstance = getFromContext(beanName, "fallback", fallback, target.type()); |
||||
return new KiccSentinelInvocationHandler(target, dispatch, new FallbackFactory.Default(fallbackInstance)); |
||||
} |
||||
|
||||
if (void.class != fallbackFactory) { |
||||
fallbackFactoryInstance = (FallbackFactory<?>) getFromContext(beanName, "fallbackFactory", fallbackFactory, FallbackFactory.class); |
||||
return new KiccSentinelInvocationHandler(target, dispatch, fallbackFactoryInstance); |
||||
} |
||||
return new KiccSentinelInvocationHandler(target, dispatch); |
||||
} |
||||
|
||||
private Object getFromContext(String name, String type, Class<?> fallbackType, Class<?> targetType) { |
||||
Object fallbackInstance = feignContext.getInstance(name, fallbackType); |
||||
if (fallbackInstance == null) { |
||||
throw new IllegalStateException(String.format( |
||||
"No %s instance of type %s found for feign client %s", type, fallbackType, name)); |
||||
} |
||||
|
||||
if (!targetType.isAssignableFrom(fallbackType)) { |
||||
throw new IllegalStateException(String.format( |
||||
"Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", |
||||
type, fallbackType, targetType, name)); |
||||
} |
||||
return fallbackInstance; |
||||
} |
||||
}); |
||||
|
||||
super.contract(new SentinelContractHolder(contract)); |
||||
return super.build(); |
||||
} |
||||
|
||||
private Object getFieldValue(Object instance, String fieldName) { |
||||
Field field = ReflectionUtils.findField(instance.getClass(), fieldName); |
||||
field.setAccessible(true); |
||||
try { |
||||
return field.get(instance); |
||||
} |
||||
catch (IllegalAccessException e) { |
||||
// ignore
|
||||
} |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { |
||||
this.applicationContext = applicationContext; |
||||
feignContext = this.applicationContext.getBean(FeignContext.class); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
package com.cloud.kicc.common.feign.sentinel.ext; |
||||
|
||||
import com.alibaba.cloud.sentinel.SentinelProperties; |
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebInterceptor; |
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; |
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler; |
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; |
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner; |
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import java.util.Optional; |
||||
|
||||
/** |
||||
*<p> |
||||
* 避免哨兵拦截请求 与 spring cloud 2021 不兼容 的问题 |
||||
* 重新注入Sentinel拦截核心Bean |
||||
* 会出现:The dependencies of some of the beans in the application context form a cycle 循环依赖问题 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) |
||||
public class KiccSentinelFilterConfiguration { |
||||
|
||||
@Bean |
||||
public SentinelWebInterceptor sentinelWebInterceptor(SentinelWebMvcConfig sentinelWebMvcConfig) { |
||||
return new SentinelWebInterceptor(sentinelWebMvcConfig); |
||||
} |
||||
|
||||
@Bean |
||||
public SentinelWebMvcConfig sentinelWebMvcConfig(SentinelProperties properties, |
||||
Optional<UrlCleaner> urlCleanerOptional, Optional<BlockExceptionHandler> blockExceptionHandlerOptional, |
||||
Optional<RequestOriginParser> requestOriginParserOptional) { |
||||
SentinelWebMvcConfig sentinelWebMvcConfig = new SentinelWebMvcConfig(); |
||||
sentinelWebMvcConfig.setHttpMethodSpecify(properties.getHttpMethodSpecify()); |
||||
sentinelWebMvcConfig.setWebContextUnify(properties.getWebContextUnify()); |
||||
|
||||
if (blockExceptionHandlerOptional.isPresent()) { |
||||
blockExceptionHandlerOptional.ifPresent(sentinelWebMvcConfig::setBlockExceptionHandler); |
||||
} else { |
||||
if (StringUtils.hasText(properties.getBlockPage())) { |
||||
sentinelWebMvcConfig.setBlockExceptionHandler( |
||||
((request, response, e) -> response.sendRedirect(properties.getBlockPage()))); |
||||
} else { |
||||
sentinelWebMvcConfig.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); |
||||
} |
||||
} |
||||
|
||||
urlCleanerOptional.ifPresent(sentinelWebMvcConfig::setUrlCleaner); |
||||
requestOriginParserOptional.ifPresent(sentinelWebMvcConfig::setOriginParser); |
||||
return sentinelWebMvcConfig; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,178 @@
@@ -0,0 +1,178 @@
|
||||
package com.cloud.kicc.common.feign.sentinel.ext; |
||||
|
||||
import com.alibaba.cloud.sentinel.feign.SentinelContractHolder; |
||||
import com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler; |
||||
import com.alibaba.csp.sentinel.Entry; |
||||
import com.alibaba.csp.sentinel.EntryType; |
||||
import com.alibaba.csp.sentinel.SphU; |
||||
import com.alibaba.csp.sentinel.Tracer; |
||||
import com.alibaba.csp.sentinel.context.ContextUtil; |
||||
import com.alibaba.csp.sentinel.slots.block.BlockException; |
||||
import com.cloud.kicc.common.core.api.R; |
||||
import feign.Feign; |
||||
import feign.InvocationHandlerFactory; |
||||
import feign.MethodMetadata; |
||||
import feign.Target; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.cloud.openfeign.FallbackFactory; |
||||
|
||||
import java.lang.reflect.InvocationHandler; |
||||
import java.lang.reflect.InvocationTargetException; |
||||
import java.lang.reflect.Method; |
||||
import java.lang.reflect.Proxy; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
|
||||
import static feign.Util.checkNotNull; |
||||
|
||||
/** |
||||
*<p> |
||||
* 支持自动降级注入 重写 {@link com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler} |
||||
* 重新写入一些提示降级信息 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@Slf4j |
||||
public class KiccSentinelInvocationHandler implements InvocationHandler { |
||||
|
||||
public static final String EQUALS = "equals"; |
||||
|
||||
public static final String HASH_CODE = "hashCode"; |
||||
|
||||
public static final String TO_STRING = "toString"; |
||||
|
||||
private final Target<?> target; |
||||
|
||||
private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch; |
||||
|
||||
private FallbackFactory<?> fallbackFactory; |
||||
|
||||
private Map<Method, Method> fallbackMethodMap; |
||||
|
||||
KiccSentinelInvocationHandler(Target<?> target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, |
||||
FallbackFactory<?> fallbackFactory) { |
||||
this.target = checkNotNull(target, "target"); |
||||
this.dispatch = checkNotNull(dispatch, "dispatch"); |
||||
this.fallbackFactory = fallbackFactory; |
||||
this.fallbackMethodMap = toFallbackMethod(dispatch); |
||||
} |
||||
|
||||
KiccSentinelInvocationHandler(Target<?> target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) { |
||||
this.target = checkNotNull(target, "target"); |
||||
this.dispatch = checkNotNull(dispatch, "dispatch"); |
||||
} |
||||
|
||||
@Override |
||||
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { |
||||
if (EQUALS.equals(method.getName())) { |
||||
try { |
||||
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; |
||||
return equals(otherHandler); |
||||
} |
||||
catch (IllegalArgumentException e) { |
||||
return false; |
||||
} |
||||
} |
||||
else if (HASH_CODE.equals(method.getName())) { |
||||
return hashCode(); |
||||
} |
||||
else if (TO_STRING.equals(method.getName())) { |
||||
return toString(); |
||||
} |
||||
|
||||
Object result; |
||||
InvocationHandlerFactory.MethodHandler methodHandler = this.dispatch.get(method); |
||||
// only handle by HardCodedTarget
|
||||
if (target instanceof Target.HardCodedTarget) { |
||||
Target.HardCodedTarget<?> hardCodedTarget = (Target.HardCodedTarget) target; |
||||
MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP |
||||
.get(hardCodedTarget.type().getName() + Feign.configKey(hardCodedTarget.type(), method)); |
||||
// resource default is HttpMethod:protocol://url
|
||||
if (methodMetadata == null) { |
||||
result = methodHandler.invoke(args); |
||||
} |
||||
else { |
||||
String resourceName = methodMetadata.template().method().toUpperCase() + ':' + hardCodedTarget.url() |
||||
+ methodMetadata.template().path(); |
||||
Entry entry = null; |
||||
try { |
||||
ContextUtil.enter(resourceName); |
||||
entry = SphU.entry(resourceName, EntryType.OUT, 1, args); |
||||
result = methodHandler.invoke(args); |
||||
} |
||||
catch (Throwable ex) { |
||||
// fallback handle
|
||||
if (!BlockException.isBlockException(ex)) { |
||||
Tracer.trace(ex); |
||||
} |
||||
if (fallbackFactory != null) { |
||||
try { |
||||
return fallbackMethodMap.get(method).invoke(fallbackFactory.create(ex), args); |
||||
} |
||||
catch (IllegalAccessException e) { |
||||
// shouldn't happen as method is public due to being an
|
||||
// interface
|
||||
throw new AssertionError(e); |
||||
} |
||||
catch (InvocationTargetException e) { |
||||
throw new AssertionError(e.getCause()); |
||||
} |
||||
} |
||||
else { |
||||
// 若是R类型 执行自动降级返回R
|
||||
if (R.class == method.getReturnType()) { |
||||
log.error("feign 服务间调用异常", ex); |
||||
return R.error(ex.getLocalizedMessage()); |
||||
} |
||||
else { |
||||
throw ex; |
||||
} |
||||
} |
||||
} |
||||
finally { |
||||
if (entry != null) { |
||||
entry.exit(1, args); |
||||
} |
||||
ContextUtil.exit(); |
||||
} |
||||
} |
||||
} |
||||
else { |
||||
// other target type using default strategy
|
||||
result = methodHandler.invoke(args); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (obj instanceof SentinelInvocationHandler) { |
||||
KiccSentinelInvocationHandler other = (KiccSentinelInvocationHandler) obj; |
||||
return target.equals(other.target); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return target.hashCode(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return target.toString(); |
||||
} |
||||
|
||||
static Map<Method, Method> toFallbackMethod(Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) { |
||||
Map<Method, Method> result = new LinkedHashMap<>(); |
||||
for (Method method : dispatch.keySet()) { |
||||
method.setAccessible(true); |
||||
result.put(method, method); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,122 @@
@@ -0,0 +1,122 @@
|
||||
package com.cloud.kicc.common.feign.sentinel.handle; |
||||
|
||||
import com.alibaba.csp.sentinel.Tracer; |
||||
import com.cloud.kicc.common.core.api.R; |
||||
import com.cloud.kicc.common.core.exception.CheckedException; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.security.access.AccessDeniedException; |
||||
import org.springframework.security.core.SpringSecurityMessageSource; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.validation.BindException; |
||||
import org.springframework.validation.FieldError; |
||||
import org.springframework.web.bind.MethodArgumentNotValidException; |
||||
import org.springframework.web.bind.annotation.ExceptionHandler; |
||||
import org.springframework.web.bind.annotation.ResponseStatus; |
||||
import org.springframework.web.bind.annotation.RestControllerAdvice; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
*<p> |
||||
* 全局异常处理器结合sentinel 全局异常处理器不能作用在 oauth server |
||||
* 因为授权那边有自己的异常处理不能覆盖授权异常处理 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/18 |
||||
*/ |
||||
@Slf4j |
||||
@RestControllerAdvice |
||||
@ConditionalOnExpression("!'${security.oauth2.client.clientId}'.isEmpty()") |
||||
public class GlobalBizExceptionHandler { |
||||
|
||||
/** |
||||
* 全局异常. |
||||
* @param e the e |
||||
* @return R |
||||
*/ |
||||
@ExceptionHandler(Exception.class) |
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) |
||||
public R handleGlobalException(Exception e) { |
||||
log.error("全局异常信息 ex={}", e.getMessage(), e); |
||||
|
||||
// 业务异常交由 sentinel 记录
|
||||
Tracer.trace(e); |
||||
return R.error(e.getLocalizedMessage()); |
||||
} |
||||
|
||||
/** |
||||
* 通用前端错误提示自定义全局异常 |
||||
* @param e the e |
||||
* @return R |
||||
*/ |
||||
@ExceptionHandler(CheckedException.class) |
||||
@ResponseStatus(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED) |
||||
public R handleGlobalCommonException(CheckedException e) { |
||||
log.error("自定义异常信息 ex={}", e.getMessage(), e); |
||||
|
||||
// 业务异常交由 sentinel 记录
|
||||
Tracer.trace(e); |
||||
return R.error(e.getLocalizedMessage()); |
||||
} |
||||
|
||||
/** |
||||
* 处理业务校验过程中碰到的非法参数异常 该异常基本由{@link org.springframework.util.Assert}抛出 |
||||
* @see Assert#hasLength(String, String) |
||||
* @see Assert#hasText(String, String) |
||||
* @see Assert#isTrue(boolean, String) |
||||
* @see Assert#isNull(Object, String) |
||||
* @see Assert#notNull(Object, String) |
||||
* @param exception 参数校验异常 |
||||
* @return API返回结果对象包装后的错误输出结果 |
||||
*/ |
||||
@ExceptionHandler(IllegalArgumentException.class) |
||||
@ResponseStatus(HttpStatus.OK) |
||||
public R handleIllegalArgumentException(IllegalArgumentException exception) { |
||||
log.error("非法参数,ex = {}", exception.getMessage(), exception); |
||||
return R.error(exception.getMessage()); |
||||
} |
||||
|
||||
/** |
||||
* AccessDeniedException |
||||
* @param e the e |
||||
* @return R |
||||
*/ |
||||
@ExceptionHandler(AccessDeniedException.class) |
||||
@ResponseStatus(HttpStatus.FORBIDDEN) |
||||
public R handleAccessDeniedException(AccessDeniedException e) { |
||||
String msg = SpringSecurityMessageSource.getAccessor().getMessage("AbstractAccessDecisionManager.accessDenied", |
||||
e.getMessage()); |
||||
log.warn("拒绝授权异常信息 ex={}", msg); |
||||
return R.error(e.getLocalizedMessage()); |
||||
} |
||||
|
||||
/** |
||||
* validation Exception |
||||
* @param exception |
||||
* @return R |
||||
*/ |
||||
@ExceptionHandler({ MethodArgumentNotValidException.class }) |
||||
@ResponseStatus(HttpStatus.BAD_REQUEST) |
||||
public R handleBodyValidException(MethodArgumentNotValidException exception) { |
||||
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors(); |
||||
log.warn("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage()); |
||||
return R.error(fieldErrors.get(0).getDefaultMessage()); |
||||
} |
||||
|
||||
/** |
||||
* validation Exception (以form-data形式传参) |
||||
* @param exception |
||||
* @return R |
||||
*/ |
||||
@ExceptionHandler({ BindException.class }) |
||||
@ResponseStatus(HttpStatus.BAD_REQUEST) |
||||
public R bindExceptionHandler(BindException exception) { |
||||
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors(); |
||||
log.warn("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage()); |
||||
return R.error(fieldErrors.get(0).getDefaultMessage()); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
package com.cloud.kicc.common.feign.sentinel.handle; |
||||
|
||||
import cn.hutool.http.ContentType; |
||||
import cn.hutool.json.JSONUtil; |
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; |
||||
import com.alibaba.csp.sentinel.slots.block.BlockException; |
||||
import com.cloud.kicc.common.core.api.R; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.http.HttpStatus; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
/** |
||||
*<p> |
||||
* sentinel统一降级限流返回处理 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@Slf4j |
||||
public class KiccUrlBlockHandler implements BlockExceptionHandler { |
||||
|
||||
@Override |
||||
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { |
||||
log.error("sentinel 降级 资源名称{}", e.getRule().getResource(), e); |
||||
|
||||
response.setContentType(ContentType.JSON.toString()); |
||||
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); |
||||
response.getWriter().print(JSONUtil.toJsonStr(R.error(e.getMessage()))); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
package com.cloud.kicc.common.feign.sentinel.parser; |
||||
|
||||
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
|
||||
/** |
||||
*<p> |
||||
* sentinel 请求头解析判断 |
||||
* 配置Sentinel授权规则,根据什么条件进行判断 |
||||
* 白名单/黑名单 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
public class KiccHeaderRequestOriginParser implements RequestOriginParser { |
||||
|
||||
/** |
||||
* 请求头获取allow |
||||
*/ |
||||
private static final String ALLOW = "Allow"; |
||||
|
||||
/** |
||||
* Parse the origin from given HTTP request. |
||||
* @param request HTTP request |
||||
* @return parsed origin |
||||
*/ |
||||
@Override |
||||
public String parseOrigin(HttpServletRequest request) { |
||||
return request.getHeader(ALLOW); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,223 @@
@@ -0,0 +1,223 @@
|
||||
package org.springframework.cloud.openfeign; |
||||
|
||||
import com.cloud.kicc.common.feign.KiccFeignAutoConfiguration; |
||||
import lombok.Getter; |
||||
import org.springframework.beans.factory.BeanClassLoaderAware; |
||||
import org.springframework.beans.factory.config.BeanDefinitionHolder; |
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition; |
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder; |
||||
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; |
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry; |
||||
import org.springframework.context.EnvironmentAware; |
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; |
||||
import org.springframework.core.annotation.AnnotatedElementUtils; |
||||
import org.springframework.core.annotation.AnnotationAttributes; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.core.io.support.SpringFactoriesLoader; |
||||
import org.springframework.core.type.AnnotationMetadata; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
*<p> |
||||
* feign 自动配置功能内置基于mica |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
public class KiccFeignClientsRegistrar implements ImportBeanDefinitionRegistrar, BeanClassLoaderAware, EnvironmentAware { |
||||
|
||||
@Getter |
||||
private ClassLoader beanClassLoader; |
||||
|
||||
@Getter |
||||
private Environment environment; |
||||
|
||||
@Override |
||||
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { |
||||
registerFeignClients(registry); |
||||
} |
||||
|
||||
@Override |
||||
public void setBeanClassLoader(ClassLoader classLoader) { |
||||
this.beanClassLoader = classLoader; |
||||
} |
||||
|
||||
private void registerFeignClients(BeanDefinitionRegistry registry) { |
||||
List<String> feignClients = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); |
||||
// 如果 spring.factories 里为空
|
||||
if (feignClients.isEmpty()) { |
||||
return; |
||||
} |
||||
for (String className : feignClients) { |
||||
try { |
||||
Class<?> clazz = beanClassLoader.loadClass(className); |
||||
AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(clazz, FeignClient.class); |
||||
if (attributes == null) { |
||||
continue; |
||||
} |
||||
// 如果已经存在该 bean,支持原生的 Feign
|
||||
if (registry.containsBeanDefinition(className)) { |
||||
continue; |
||||
} |
||||
registerClientConfiguration(registry, getClientName(attributes), attributes.get("configuration")); |
||||
|
||||
validate(attributes); |
||||
BeanDefinitionBuilder definition = BeanDefinitionBuilder |
||||
.genericBeanDefinition(FeignClientFactoryBean.class); |
||||
definition.addPropertyValue("url", getUrl(attributes)); |
||||
definition.addPropertyValue("path", getPath(attributes)); |
||||
String name = getName(attributes); |
||||
definition.addPropertyValue("name", name); |
||||
|
||||
// 兼容最新版本的 spring-cloud-openfeign,尚未发布
|
||||
StringBuilder aliasBuilder = new StringBuilder(18); |
||||
if (attributes.containsKey("contextId")) { |
||||
String contextId = getContextId(attributes); |
||||
aliasBuilder.append(contextId); |
||||
definition.addPropertyValue("contextId", contextId); |
||||
} |
||||
else { |
||||
aliasBuilder.append(name); |
||||
} |
||||
|
||||
definition.addPropertyValue("type", className); |
||||
definition.addPropertyValue("decode404", attributes.get("decode404")); |
||||
definition.addPropertyValue("fallback", attributes.get("fallback")); |
||||
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); |
||||
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); |
||||
|
||||
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); |
||||
|
||||
// alias
|
||||
String alias = aliasBuilder.append("FeignClient").toString(); |
||||
|
||||
// has a default, won't be null
|
||||
boolean primary = (Boolean) attributes.get("primary"); |
||||
|
||||
beanDefinition.setPrimary(primary); |
||||
|
||||
String qualifier = getQualifier(attributes); |
||||
if (StringUtils.hasText(qualifier)) { |
||||
alias = qualifier; |
||||
} |
||||
|
||||
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, |
||||
new String[] { alias }); |
||||
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); |
||||
|
||||
} |
||||
catch (ClassNotFoundException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Return the class used by {@link SpringFactoriesLoader} to load configuration |
||||
* candidates. |
||||
* @return the factory class |
||||
*/ |
||||
private Class<?> getSpringFactoriesLoaderFactoryClass() { |
||||
return KiccFeignAutoConfiguration.class; |
||||
} |
||||
|
||||
private void validate(Map<String, Object> attributes) { |
||||
AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes); |
||||
// This blows up if an aliased property is overspecified
|
||||
FeignClientsRegistrar.validateFallback(annotation.getClass("fallback")); |
||||
FeignClientsRegistrar.validateFallbackFactory(annotation.getClass("fallbackFactory")); |
||||
} |
||||
|
||||
private String getName(Map<String, Object> attributes) { |
||||
String name = (String) attributes.get("serviceId"); |
||||
if (!StringUtils.hasText(name)) { |
||||
name = (String) attributes.get("name"); |
||||
} |
||||
if (!StringUtils.hasText(name)) { |
||||
name = (String) attributes.get("value"); |
||||
} |
||||
name = resolve(name); |
||||
return FeignClientsRegistrar.getName(name); |
||||
} |
||||
|
||||
private String getContextId(Map<String, Object> attributes) { |
||||
String contextId = (String) attributes.get("contextId"); |
||||
if (!StringUtils.hasText(contextId)) { |
||||
return getName(attributes); |
||||
} |
||||
|
||||
contextId = resolve(contextId); |
||||
return FeignClientsRegistrar.getName(contextId); |
||||
} |
||||
|
||||
private String resolve(String value) { |
||||
if (StringUtils.hasText(value)) { |
||||
return this.environment.resolvePlaceholders(value); |
||||
} |
||||
return value; |
||||
} |
||||
|
||||
private String getUrl(Map<String, Object> attributes) { |
||||
String url = resolve((String) attributes.get("url")); |
||||
return FeignClientsRegistrar.getUrl(url); |
||||
} |
||||
|
||||
private String getPath(Map<String, Object> attributes) { |
||||
String path = resolve((String) attributes.get("path")); |
||||
return FeignClientsRegistrar.getPath(path); |
||||
} |
||||
|
||||
@Nullable |
||||
private String getQualifier(@Nullable Map<String, Object> client) { |
||||
if (client == null) { |
||||
return null; |
||||
} |
||||
String qualifier = (String) client.get("qualifier"); |
||||
if (StringUtils.hasText(qualifier)) { |
||||
return qualifier; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
@Nullable |
||||
private String getClientName(@Nullable Map<String, Object> client) { |
||||
if (client == null) { |
||||
return null; |
||||
} |
||||
String value = (String) client.get("contextId"); |
||||
if (!StringUtils.hasText(value)) { |
||||
value = (String) client.get("value"); |
||||
} |
||||
if (!StringUtils.hasText(value)) { |
||||
value = (String) client.get("name"); |
||||
} |
||||
if (!StringUtils.hasText(value)) { |
||||
value = (String) client.get("serviceId"); |
||||
} |
||||
if (StringUtils.hasText(value)) { |
||||
return value; |
||||
} |
||||
|
||||
throw new IllegalStateException( |
||||
"Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName()); |
||||
} |
||||
|
||||
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) { |
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class); |
||||
builder.addConstructorArgValue(name); |
||||
builder.addConstructorArgValue(configuration); |
||||
registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), |
||||
builder.getBeanDefinition()); |
||||
} |
||||
|
||||
@Override |
||||
public void setEnvironment(Environment environment) { |
||||
this.environment = environment; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
||||
com.cloud.kicc.common.feign.KiccFeignAutoConfiguration,\ |
||||
com.cloud.kicc.common.feign.sentinel.SentinelAutoConfiguration,\ |
||||
com.cloud.kicc.common.feign.sentinel.handle.GlobalBizExceptionHandler |
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-tool</artifactId> |
||||
<version>1.0.0</version> |
||||
</parent> |
||||
|
||||
<artifactId>kicc-common-job</artifactId> |
||||
<packaging>jar</packaging> |
||||
|
||||
<description>kicc 定时任务,基于xxl-job</description> |
||||
|
||||
<!--考虑这个作为一个单模块使用,后续引入依赖需要注意低耦合--> |
||||
<dependencies> |
||||
<!--xxl job--> |
||||
<dependency> |
||||
<groupId>com.xuxueli</groupId> |
||||
<artifactId>xxl-job-core</artifactId> |
||||
<version>${xxl-job.version}</version> |
||||
</dependency> |
||||
<!--提供服务发现能力--> |
||||
<dependency> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-commons</artifactId> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
package com.cloud.kicc.common.job; |
||||
|
||||
import com.cloud.kicc.common.job.properties.XxlExecutorProperties; |
||||
import com.cloud.kicc.common.job.properties.XxlJobProperties; |
||||
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; |
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
||||
import org.springframework.cloud.client.discovery.DiscoveryClient; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import java.util.stream.Collectors; |
||||
|
||||
/** |
||||
*<p> |
||||
* xxl-job自动装配 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/19 |
||||
*/ |
||||
@Configuration(proxyBeanMethods = false) |
||||
@EnableConfigurationProperties(XxlJobProperties.class) |
||||
public class XxlJobAutoConfiguration { |
||||
|
||||
/** |
||||
* 服务名称 包含 XXL_JOB_ADMIN 则说明是 Admin |
||||
*/ |
||||
private static final String XXL_JOB_ADMIN = "xxl-job-admin"; |
||||
|
||||
/** |
||||
* 配置xxl-job 执行器,提供自动发现 xxl-job-admin 能力 |
||||
* @param xxlJobProperties xxl 配置 |
||||
* @param environment 环境变量 |
||||
* @param discoveryClient 注册发现客户端 |
||||
* @return |
||||
*/ |
||||
@Bean |
||||
public XxlJobSpringExecutor xxlJobSpringExecutor(XxlJobProperties xxlJobProperties, Environment environment, |
||||
DiscoveryClient discoveryClient) { |
||||
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); |
||||
XxlExecutorProperties executor = xxlJobProperties.getExecutor(); |
||||
// 应用名默认为服务名
|
||||
String appName = executor.getAppname(); |
||||
if (!StringUtils.hasText(appName)) { |
||||
appName = environment.getProperty("spring.application.name"); |
||||
} |
||||
xxlJobSpringExecutor.setAppname(appName); |
||||
xxlJobSpringExecutor.setAddress(executor.getAddress()); |
||||
xxlJobSpringExecutor.setIp(executor.getIp()); |
||||
xxlJobSpringExecutor.setPort(executor.getPort()); |
||||
xxlJobSpringExecutor.setAccessToken(executor.getAccessToken()); |
||||
xxlJobSpringExecutor.setLogPath(executor.getLogPath()); |
||||
xxlJobSpringExecutor.setLogRetentionDays(executor.getLogRetentionDays()); |
||||
|
||||
// 如果配置为空则获取注册中心的服务列表 "http://127.0.0.1:8057/xxl-job-admin"
|
||||
if (!StringUtils.hasText(xxlJobProperties.getAdmin().getAddresses())) { |
||||
String serverList = discoveryClient.getServices().stream().filter(s -> s.contains(XXL_JOB_ADMIN)) |
||||
.flatMap(s -> discoveryClient.getInstances(s).stream()).map(instance -> String |
||||
.format("http://%s:%s/%s", instance.getHost(), instance.getPort(), XXL_JOB_ADMIN)) |
||||
.collect(Collectors.joining(",")); |
||||
xxlJobSpringExecutor.setAdminAddresses(serverList); |
||||
} |
||||
else { |
||||
xxlJobSpringExecutor.setAdminAddresses(xxlJobProperties.getAdmin().getAddresses()); |
||||
} |
||||
|
||||
return xxlJobSpringExecutor; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
package com.cloud.kicc.common.job.annotation; |
||||
|
||||
import com.cloud.kicc.common.job.XxlJobAutoConfiguration; |
||||
import org.springframework.context.annotation.Import; |
||||
|
||||
import java.lang.annotation.*; |
||||
|
||||
/** |
||||
*<p> |
||||
* 激活xxl-job配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/19 |
||||
*/ |
||||
@Target({ ElementType.TYPE }) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Documented |
||||
@Inherited |
||||
@Import({ XxlJobAutoConfiguration.class }) |
||||
public @interface EnableKiccXxlJob { |
||||
|
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
package com.cloud.kicc.common.job.properties; |
||||
|
||||
import lombok.Data; |
||||
|
||||
/** |
||||
*<p> |
||||
* xxl-job管理平台配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/19 |
||||
*/ |
||||
@Data |
||||
public class XxlAdminProperties { |
||||
|
||||
/** |
||||
* 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。 执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; |
||||
*/ |
||||
private String addresses; |
||||
|
||||
} |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
package com.cloud.kicc.common.job.properties; |
||||
|
||||
import lombok.Data; |
||||
|
||||
/** |
||||
*<p> |
||||
* xxl-job执行器配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/19 |
||||
*/ |
||||
@Data |
||||
public class XxlExecutorProperties { |
||||
|
||||
/** |
||||
* 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 |
||||
*/ |
||||
private String appname; |
||||
|
||||
/** |
||||
* 服务注册地址,优先使用该配置作为注册地址 为空时使用内嵌服务 ”IP:PORT“ 作为注册地址 从而更灵活的支持容器类型执行器动态IP和动态映射端口问题 |
||||
*/ |
||||
private String address; |
||||
|
||||
/** |
||||
* 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP ,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 |
||||
* "调度中心请求并触发任务" |
||||
*/ |
||||
private String ip; |
||||
|
||||
/** |
||||
* 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口; |
||||
*/ |
||||
private Integer port = 0; |
||||
|
||||
/** |
||||
* 执行器通讯TOKEN [选填]:非空时启用; |
||||
*/ |
||||
private String accessToken; |
||||
|
||||
/** |
||||
* 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径; |
||||
*/ |
||||
private String logPath = "logs/applogs/xxl-job/jobhandler"; |
||||
|
||||
/** |
||||
* 执行器日志保存天数 [选填] :值大于3天时生效,启用执行器Log文件定期清理功能,否则不生效; |
||||
*/ |
||||
private Integer logRetentionDays = 30; |
||||
|
||||
} |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
package com.cloud.kicc.common.job.properties; |
||||
|
||||
import lombok.Data; |
||||
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty; |
||||
|
||||
/** |
||||
*<p> |
||||
* xxl-job配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/19 |
||||
*/ |
||||
@Data |
||||
@ConfigurationProperties(prefix = "xxl.job") |
||||
public class XxlJobProperties { |
||||
|
||||
@NestedConfigurationProperty |
||||
private XxlAdminProperties admin = new XxlAdminProperties(); |
||||
|
||||
@NestedConfigurationProperty |
||||
private XxlExecutorProperties executor = new XxlExecutorProperties(); |
||||
|
||||
} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-tool</artifactId> |
||||
<version>1.0.0</version> |
||||
</parent> |
||||
|
||||
<artifactId>kicc-common-log</artifactId> |
||||
<packaging>jar</packaging> |
||||
|
||||
<description>kicc 日志服务</description> |
||||
|
||||
<!--考虑这个作为一个单模块使用,目前依赖了工具类核心包与system接口模块,后续引入依赖需要注意低耦合--> |
||||
<dependencies> |
||||
<!--运维监控api模块--> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-monitor-api</artifactId> |
||||
</dependency> |
||||
<!--安全依赖获取上下文信息--> |
||||
<dependency> |
||||
<groupId>org.springframework.security</groupId> |
||||
<artifactId>spring-security-core</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.security.oauth.boot</groupId> |
||||
<artifactId>spring-security-oauth2-autoconfigure</artifactId> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
package com.cloud.kicc.common.log; |
||||
|
||||
import com.cloud.kicc.common.log.aspect.SysLogAspect; |
||||
import com.cloud.kicc.common.log.event.SysLogListener; |
||||
import com.cloud.kicc.monitor.api.feign.RemoteLogService; |
||||
import lombok.RequiredArgsConstructor; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.scheduling.annotation.EnableAsync; |
||||
|
||||
/** |
||||
*<p> |
||||
* 日志自动配置 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@EnableAsync |
||||
@RequiredArgsConstructor |
||||
@ConditionalOnWebApplication |
||||
@Configuration(proxyBeanMethods = false) |
||||
public class LogAutoConfiguration { |
||||
|
||||
@Bean |
||||
public SysLogListener sysLogListener(RemoteLogService remoteLogService) { |
||||
return new SysLogListener(remoteLogService); |
||||
} |
||||
|
||||
@Bean |
||||
public SysLogAspect sysLogAspect() { |
||||
return new SysLogAspect(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
package com.cloud.kicc.common.log.annotation; |
||||
|
||||
import java.lang.annotation.*; |
||||
|
||||
/** |
||||
*<p> |
||||
* 操作日志注解 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@Target(ElementType.METHOD) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Documented |
||||
public @interface SysLog { |
||||
|
||||
/** |
||||
* 描述 |
||||
* @return {String} |
||||
*/ |
||||
String value(); |
||||
|
||||
} |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
package com.cloud.kicc.common.log.aspect; |
||||
|
||||
import com.cloud.kicc.common.core.util.SpringContextHolderUtil; |
||||
import com.cloud.kicc.common.log.annotation.SysLog; |
||||
import com.cloud.kicc.common.log.event.SysLogEvent; |
||||
import com.cloud.kicc.common.log.menus.LogTypeEnum; |
||||
import com.cloud.kicc.common.log.util.SysLogUtils; |
||||
import com.cloud.kicc.monitor.api.entity.OperLog; |
||||
import lombok.SneakyThrows; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.aspectj.lang.ProceedingJoinPoint; |
||||
import org.aspectj.lang.annotation.Around; |
||||
import org.aspectj.lang.annotation.Aspect; |
||||
|
||||
/** |
||||
*<p> |
||||
* 操作日志使用spring event异步入库 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@Aspect |
||||
@Slf4j |
||||
public class SysLogAspect { |
||||
|
||||
@Around("@annotation(sysLog)") |
||||
@SneakyThrows |
||||
public Object around(ProceedingJoinPoint point, SysLog sysLog) { |
||||
Long startTime = System.currentTimeMillis(); |
||||
String strClassName = point.getTarget().getClass().getName(); |
||||
String strMethodName = point.getSignature().getName(); |
||||
log.debug("[类名]:{},[方法]:{}", strClassName, strMethodName); |
||||
OperLog operLog = SysLogUtils.getSysLog(); |
||||
operLog.setTitle(sysLog.value()); |
||||
Object obj; |
||||
try { |
||||
obj = point.proceed(); |
||||
} catch (Exception e) { |
||||
operLog.setType(LogTypeEnum.ERROR.getType()); |
||||
operLog.setErrorMsg(e.getMessage()); |
||||
throw e; |
||||
} finally { |
||||
Long endTime = System.currentTimeMillis(); |
||||
operLog.setExecuteTime((endTime - startTime) + "毫秒"); |
||||
// 发送异步日志事件
|
||||
SpringContextHolderUtil.publishEvent(new SysLogEvent(operLog)); |
||||
} |
||||
return obj; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
package com.cloud.kicc.common.log.event; |
||||
|
||||
import com.cloud.kicc.monitor.api.entity.OperLog; |
||||
import org.springframework.context.ApplicationEvent; |
||||
|
||||
/** |
||||
*<p> |
||||
* 系统日志事件 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
public class SysLogEvent extends ApplicationEvent { |
||||
|
||||
public SysLogEvent(OperLog source) { |
||||
super(source); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
package com.cloud.kicc.common.log.event; |
||||
|
||||
import com.cloud.kicc.common.core.constant.SecurityConstants; |
||||
import com.cloud.kicc.monitor.api.entity.OperLog; |
||||
import com.cloud.kicc.monitor.api.feign.RemoteLogService; |
||||
import lombok.RequiredArgsConstructor; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.context.event.EventListener; |
||||
import org.springframework.core.annotation.Order; |
||||
import org.springframework.scheduling.annotation.Async; |
||||
|
||||
/** |
||||
*<p> |
||||
* 异步监听日志事件 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@Slf4j |
||||
@RequiredArgsConstructor |
||||
public class SysLogListener { |
||||
|
||||
private final RemoteLogService remoteLogService; |
||||
|
||||
@Async |
||||
@Order |
||||
@EventListener(SysLogEvent.class) |
||||
public void saveSysLog(SysLogEvent event) { |
||||
OperLog operLog = (OperLog) event.getSource(); |
||||
remoteLogService.saveLog(operLog, SecurityConstants.FROM_IN); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
package com.cloud.kicc.common.log.init; |
||||
|
||||
import org.springframework.boot.SpringApplication; |
||||
import org.springframework.boot.env.EnvironmentPostProcessor; |
||||
import org.springframework.core.Ordered; |
||||
import org.springframework.core.env.ConfigurableEnvironment; |
||||
|
||||
/** |
||||
*<p> |
||||
* 通过环境变量的形式设置springboot内部logback路径 |
||||
* 达到每个服务都可以动态配置的目的 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
public class ApplicationLoggerInitializer implements EnvironmentPostProcessor, Ordered { |
||||
|
||||
@Override |
||||
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { |
||||
String appName = environment.getProperty("spring.application.name"); |
||||
String logBase = environment.getProperty("LOGGING_PATH", "logs"); |
||||
|
||||
// spring boot admin 直接加载日志
|
||||
System.setProperty("logging.file.name", String.format("%s/%s/debug.log", logBase, appName)); |
||||
} |
||||
|
||||
@Override |
||||
public int getOrder() { |
||||
return Ordered.LOWEST_PRECEDENCE; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
package com.cloud.kicc.common.log.menus; |
||||
|
||||
import lombok.Getter; |
||||
import lombok.RequiredArgsConstructor; |
||||
|
||||
/** |
||||
*<p> |
||||
* 日志类型 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@Getter |
||||
@RequiredArgsConstructor |
||||
public enum LogTypeEnum { |
||||
|
||||
/** |
||||
* 正常日志类型 |
||||
*/ |
||||
NORMAL("0", "正常日志"), |
||||
|
||||
/** |
||||
* 错误日志类型 |
||||
*/ |
||||
ERROR("9", "错误日志"); |
||||
|
||||
/** |
||||
* 类型 |
||||
*/ |
||||
private final String type; |
||||
|
||||
/** |
||||
* 描述 |
||||
*/ |
||||
private final String description; |
||||
|
||||
} |
@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
|
||||
package com.cloud.kicc.common.log.util; |
||||
|
||||
import cn.hutool.core.util.ObjectUtil; |
||||
import cn.hutool.core.util.URLUtil; |
||||
import cn.hutool.extra.servlet.ServletUtil; |
||||
import cn.hutool.http.HttpUtil; |
||||
import com.cloud.kicc.common.data.entity.CasUser; |
||||
import com.cloud.kicc.common.log.menus.LogTypeEnum; |
||||
import com.cloud.kicc.monitor.api.entity.OperLog; |
||||
import lombok.experimental.UtilityClass; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication; |
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationConverter; |
||||
import org.springframework.web.context.request.RequestContextHolder; |
||||
import org.springframework.web.context.request.ServletRequestAttributes; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import java.time.LocalDateTime; |
||||
import java.util.Objects; |
||||
import java.util.Optional; |
||||
|
||||
/** |
||||
*<p> |
||||
* 系统日志工具类 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@UtilityClass |
||||
public class SysLogUtils { |
||||
|
||||
public OperLog getSysLog() { |
||||
HttpServletRequest request = ((ServletRequestAttributes) Objects |
||||
.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); |
||||
OperLog sysLog = new OperLog(); |
||||
sysLog.setOperIp(ServletUtil.getClientIP(request)); |
||||
sysLog.setType(LogTypeEnum.NORMAL.getType()); |
||||
sysLog.setOperAddr(ServletUtil.getClientIP(request)); |
||||
sysLog.setOperUrl(URLUtil.getPath(request.getRequestURI())); |
||||
sysLog.setMethod(request.getMethod()); |
||||
sysLog.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT)); |
||||
sysLog.setOperParam(HttpUtil.toParams(request.getParameterMap())); |
||||
sysLog.setClientId(getClientId(request)); |
||||
sysLog.setServiceId(getClientId(request)); |
||||
sysLog.setOperTime(LocalDateTime.now()); |
||||
if (ObjectUtil.isNotEmpty(getUser())) { |
||||
sysLog.setOperName(getUser().getUsername()); |
||||
sysLog.setCreateById(getUser().getId()); |
||||
sysLog.setCreateByName(getUser().getUsername()); |
||||
sysLog.setUpdateById(getUser().getId()); |
||||
sysLog.setUpdateByName(getUser().getUsername()); |
||||
} |
||||
return sysLog; |
||||
} |
||||
|
||||
/** |
||||
* 获取客户端 |
||||
* @return clientId |
||||
*/ |
||||
private String getClientId(HttpServletRequest request) { |
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
||||
if (authentication instanceof OAuth2Authentication) { |
||||
OAuth2Authentication auth2Authentication = (OAuth2Authentication) authentication; |
||||
return auth2Authentication.getOAuth2Request().getClientId(); |
||||
} |
||||
if (authentication instanceof UsernamePasswordAuthenticationToken) { |
||||
BasicAuthenticationConverter basicAuthenticationConverter = new BasicAuthenticationConverter(); |
||||
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = basicAuthenticationConverter |
||||
.convert(request); |
||||
if (usernamePasswordAuthenticationToken != null) { |
||||
return usernamePasswordAuthenticationToken.getName(); |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* 获取用户 |
||||
*/ |
||||
protected CasUser getUser() { |
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
||||
if (Optional.ofNullable(authentication).isPresent()) { |
||||
Object principal = authentication.getPrincipal(); |
||||
if (principal instanceof CasUser) { |
||||
return (CasUser) principal; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
||||
com.cloud.kicc.common.log.LogAutoConfiguration |
||||
org.springframework.boot.env.EnvironmentPostProcessor=\ |
||||
com.cloud.kicc.common.log.init.ApplicationLoggerInitializer |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-tool</artifactId> |
||||
<version>1.0.0</version> |
||||
</parent> |
||||
|
||||
<artifactId>kicc-common-mock</artifactId> |
||||
<packaging>jar</packaging> |
||||
|
||||
<description>kicc oauth 2.0 单元模拟测试工具类</description> |
||||
|
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-security</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.security</groupId> |
||||
<artifactId>spring-security-test</artifactId> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
package com.cloud.kicc.common.mock; |
||||
|
||||
import cn.hutool.http.HttpRequest; |
||||
import cn.hutool.json.JSONUtil; |
||||
import com.cloud.kicc.common.core.util.SpringContextHolderUtil; |
||||
import com.cloud.kicc.common.mock.annotation.WithMockOAuth2User; |
||||
import org.springframework.security.core.context.SecurityContext; |
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
import org.springframework.security.oauth2.client.OAuth2ClientContext; |
||||
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; |
||||
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; |
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication; |
||||
import org.springframework.security.oauth2.provider.token.RemoteTokenServices; |
||||
import org.springframework.security.test.context.support.WithSecurityContextFactory; |
||||
|
||||
/** |
||||
*<p> |
||||
* oauth2 上下文生成处理器 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
public class WithMockSecurityContextFactory implements WithSecurityContextFactory<WithMockOAuth2User> { |
||||
|
||||
@Override |
||||
public SecurityContext createSecurityContext(WithMockOAuth2User oAuth2User) { |
||||
// 1. 请求认证中心获取token
|
||||
String token = getToken(oAuth2User); |
||||
|
||||
// 2. 解析认证中心返回用户
|
||||
OAuth2Authentication authentication = getUser(token); |
||||
|
||||
// 3. 构建 oauth2 上下文
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext(); |
||||
context.setAuthentication(authentication); |
||||
|
||||
// 4. 上下文保存 token
|
||||
DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(token); |
||||
OAuth2ClientContext clientContext = SpringContextHolderUtil.getBean(OAuth2ClientContext.class); |
||||
clientContext.setAccessToken(accessToken); |
||||
return context; |
||||
} |
||||
|
||||
/** |
||||
* 请求认证中心获取token |
||||
* @param oAuth2User 账号、密码 |
||||
* @return String token |
||||
*/ |
||||
private String getToken(WithMockOAuth2User oAuth2User) { |
||||
OAuth2ProtectedResourceDetails clientProperties = SpringContextHolderUtil.getBean(OAuth2ProtectedResourceDetails.class); |
||||
String result = HttpRequest.post(clientProperties.getAccessTokenUri()) |
||||
.basicAuth(clientProperties.getClientId(), clientProperties.getClientSecret()) |
||||
.form("username", oAuth2User.username()) |
||||
.form("password", oAuth2User.password()) |
||||
.form("grant_type", "password") |
||||
.form("scope", clientProperties.getScope()) |
||||
.execute() |
||||
.body(); |
||||
return JSONUtil.parseObj(result).getStr("access_token"); |
||||
} |
||||
|
||||
/** |
||||
* 使用token 获取用户详情 |
||||
* @param token token |
||||
* @return user详细 |
||||
*/ |
||||
private OAuth2Authentication getUser(String token) { |
||||
RemoteTokenServices tokenServices = SpringContextHolderUtil.getBean(RemoteTokenServices.class); |
||||
return tokenServices.loadAuthentication(token); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
package com.cloud.kicc.common.mock.annotation; |
||||
|
||||
import com.cloud.kicc.common.core.constant.SecurityConstants; |
||||
import com.cloud.kicc.common.mock.WithMockSecurityContextFactory; |
||||
import org.springframework.security.test.context.support.WithSecurityContext; |
||||
|
||||
import java.lang.annotation.Retention; |
||||
import java.lang.annotation.RetentionPolicy; |
||||
|
||||
/** |
||||
*<p> |
||||
* WithMockOAuth2User 注解 |
||||
* 用于单元测试模拟登录用户测试接口 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@WithSecurityContext(factory = WithMockSecurityContextFactory.class) |
||||
public @interface WithMockOAuth2User { |
||||
|
||||
/** |
||||
* 用户名 |
||||
*/ |
||||
String username() default SecurityConstants.MOCK_USERNAME; |
||||
|
||||
/** |
||||
* 密码 |
||||
*/ |
||||
String password() default SecurityConstants.MOCK_PASSWORD; |
||||
|
||||
} |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
package com.cloud.kicc.common.mock.kit; |
||||
|
||||
import com.cloud.kicc.common.core.util.SpringContextHolderUtil; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.security.oauth2.client.OAuth2ClientContext; |
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor; |
||||
|
||||
/** |
||||
*<p> |
||||
* Mock 工具类 |
||||
*</p> |
||||
* |
||||
* @Author: wangxiang4 |
||||
* @Date: 2022/2/17 |
||||
*/ |
||||
public class OAuthMockKit { |
||||
|
||||
/** |
||||
* mock 请求增加统一请求头 |
||||
* @return RequestPostProcessor 类似于拦截器 |
||||
*/ |
||||
public static RequestPostProcessor token() { |
||||
return mockRequest -> { |
||||
OAuth2ClientContext clientContext = SpringContextHolderUtil.getBean(OAuth2ClientContext.class); |
||||
String token = clientContext.getAccessToken().getValue(); |
||||
mockRequest.addHeader(HttpHeaders.AUTHORIZATION, String.format("Bearer: %s", token)); |
||||
return mockRequest; |
||||
}; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-tool</artifactId> |
||||
<version>1.0.0</version> |
||||
</parent> |
||||
|
||||
<artifactId>kicc-common-rocketmq</artifactId> |
||||
<packaging>jar</packaging> |
||||
|
||||
<description>阿里 rocketmq 消息中间件</description> |
||||
|
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>com.cloud</groupId> |
||||
<artifactId>kicc-common-core</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.alibaba.cloud</groupId> |
||||
<artifactId>spring-cloud-starter-stream-rocketmq</artifactId> |
||||
</dependency> |
||||
</dependencies> |
||||
|
||||
|
||||
</project> |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue