Compare commits

..

15 Commits

Author SHA1 Message Date
1b27107d22 release: v2.8.0 2024-12-25 21:12:32 +08:00
c3df4d7ef6 chore: 优化 Issue 模板 2024-12-25 21:10:30 +08:00
7ff26c4596 refactor(log): 优化包结构 2024-12-25 20:34:45 +08:00
25f499de7e refactor(extension/tenant): 修复部分 Sonar 警告 2024-12-25 20:31:21 +08:00
小熊
f7ed2bbfb0 chore: 更新租户忽略注解 (#9) 2024-12-25 17:23:37 +08:00
88d11027dd fix(extension/tenant): 修复部分错误 2024-12-24 21:52:35 +08:00
c5cb203532 refactor(log): 优化日志模块 2024-12-24 21:50:59 +08:00
5ff9391485 chore: 升级依赖
Spring Boot 3.2.10 => 3.2.12
SnailJob 1.1.2 => 1.2.0
JustAuth 1.16.6 => 1.16.7
MyBatis Flex 1.9.7 => 1.10.3
JetCache 2.7.6 => 2.7.7
Redisson 3.36.0 => 3.41.0
CosID 2.9.9 => 2.10.1
nashorn-core 15.4 => 15.5
aws-s3 1.12.771 => 1.12.780
graceful-response 5.0.0-boot3 => 5.0.4-boot3
ip2region 3.2.6 => 3.2.12
Hutool 5.8.32 => 5.8.34
2024-12-23 22:05:58 +08:00
613599f921 refactor(extension/tenant): 优化多租户模块代码及包结构 2024-12-23 20:56:32 +08:00
0d334523e9 refactor(log): 新增 LogHandler 提升日志模块的复用性 2024-12-23 20:51:42 +08:00
265c669eda refactor(log): 优化基于 AOP 实现日志代码,调整日志整体包结构 2024-12-22 22:56:58 +08:00
小熊
c089df66cf refactor(extension/tenant): 多租户组件适配动态隔离级别 (#8) 2024-12-20 10:44:09 +08:00
liquor
7c3f15a6f6 feat(log/aop): 新增 log-aop 组件模块(基于 AOP 实现日志记录) 2024-12-11 09:03:09 +00:00
75874171db docs: 更新 README 文档 2024-12-09 13:14:26 +08:00
dc407a82cc fix(extension/crud): 修复 PageResp 手动分页计算错误
Closes #7
2024-12-06 21:53:10 +08:00
53 changed files with 1303 additions and 358 deletions

View File

@@ -47,11 +47,17 @@ body:
id: checkboxes
attributes:
label: 确认
description: 在提交 issue 之前,请确保执行过以下操作。
description: 在提交 Issue 之前,请确保执行过以下操作。
options:
- label: 阅读文档
- label: 尝试[最新版本](https://central.sonatype.com/artifact/top.continew/continew-starter/versions),仍有相同问题
required: true
- label: 根据报错信息百度或 Google 一下
- label: 阅读[使用指南](https://continew.top/starter/intro/what-is.html)
required: true
- label: 搜索是否有其他人提交过类似的 issue如果对应 issue 尚未解决,您可以先订阅关注该 issue为了方便后来者查找问题解决方法请尽量避免创建重复的 issue
- label: 查找[常见问题](https://continew.top/faq.html)
required: true
- label: 根据报错信息(自行翻译英文)百度或 Google 一下
required: true
- label: 阅读源码并在 IDE 中进行断点调试
required: false
- label: 搜索是否有其他人提交过类似的 Issue如果对应 Issue 尚未解决,您可以先订阅关注该 Issue为了方便后来者查找问题解决方法请尽量避免创建重复的 Issue
required: true

View File

@@ -34,9 +34,15 @@ body:
id: checkboxes
attributes:
label: 确认
description: 在提交 issue 之前,请确保执行过以下操作。
description: 在提交 Feature 之前,请确保执行过以下操作。
options:
- label: 阅读文档
- label: 阅读[使用指南](https://continew.top/starter/intro/what-is.html)
required: true
- label: 搜索是否有其他人提交过类似的 issue如果对应 issue 尚未解决,您可以先订阅关注该 issue为了方便后来者查找问题解决方法请尽量避免创建重复的 issue
required: true
- label: 查找[常见问题](https://continew.top/faq.html)
required: true
- label: 查看[需求墙](https://continew.top/require.html)计划
required: true
- label: 搜索是否有其他人提交过类似的 Feature如果对应 Feature 尚未完成,您可以先订阅关注该 Feature为了方便后来者查找问题解决方法请尽量避免创建重复的 Feature
required: true
- label: 您是否愿意为您提出的 Feature 提交 PR
required: false

BIN
.image/qrcode.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -1,3 +1,34 @@
## [v2.8.0](https://github.com/continew-org/continew-starter/compare/v2.7.5...v2.8.0) (2024-12-25)
### ✨ 新特性
- 【log/aop】新增 log-aop 组件模块(基于 AOP 实现日志记录) (Gitee#36@dom-w) ([7c3f15a](https://github.com/continew-org/continew-starter/commit/7c3f15a6f647afabb061e560ad3335d47806d33f)) ([265c669](https://github.com/continew-org/continew-starter/commit/265c669eda7599172cc189c96428629423158e86))
### 💎 功能优化
- 【log】新增 LogHandler 提升日志模块的复用性 ([0d33452](https://github.com/continew-org/continew-starter/commit/0d334523e9d18b548740af6583521b47b8171446)) ([c5cb203](https://github.com/continew-org/continew-starter/commit/c5cb203532ea89b497121246f11ad858f1c3ac79)) ([7ff26c4](https://github.com/continew-org/continew-starter/commit/7ff26c45962e916370aeaeaa547974dbf727fdb4))
- 【extension/tenant】多租户组件适配动态隔离级别 (GitHub#8@xtanyu) ([c089df6](https://github.com/continew-org/continew-starter/commit/c089df66cf226aa8062dc7ac2c82fb0111cfc5c0)) ([613599f](https://github.com/continew-org/continew-starter/commit/613599f92199e0cde11896d41c2d090bfdc46dd3)) ([88d1102](https://github.com/continew-org/continew-starter/commit/88d11027dd18eab5a0a71f85b135a1ddc0f3942f)) ([f7ed2bb](https://github.com/continew-org/continew-starter/commit/f7ed2bbfb017667253ec50341a753b89d65562bb)) ([25f499d](https://github.com/continew-org/continew-starter/commit/25f499de7eb59b53548d9d3f6826358b2fd0c40b))
### 🐛 问题修复
- 【extension/crud】修复 PageResp 手动分页计算错误 ([dc407a8](https://github.com/continew-org/continew-starter/commit/dc407a82cca016db6896104804eef9b660d9d5a1))
### 📦 依赖升级
- Spring Boot 3.2.7 => 3.2.10 ([5ff9391](https://github.com/continew-org/continew-starter/commit/5ff93914854098ac05bf24559336e2155b8f1503))
- Spring Boot 3.2.10 => 3.2.12
- SnailJob 1.1.2 => 1.2.0
- JustAuth 1.16.6 => 1.16.7
- MyBatis Flex 1.9.7 => 1.10.3
- JetCache 2.7.6 => 2.7.7
- Redisson 3.36.0 => 3.41.0
- CosID 2.9.9 => 2.10.1
- nashorn-core 15.4 => 15.5
- aws-s3 1.12.771 => 1.12.780
- graceful-response 5.0.0-boot3 => 5.0.4-boot3
- ip2region 3.2.6 => 3.2.12
- Hutool 5.8.32 => 5.8.34
## [v2.7.5](https://github.com/continew-org/continew-starter/compare/v2.7.4...v2.7.5) (2024-12-06)
### 💎 功能优化

View File

@@ -1,34 +1,38 @@
# ContiNew Starter
<a href="https://github.com/continew-org/continew-starter/blob/dev/LICENSE" target="_blank">
<img src="https://img.shields.io/badge/License-LGPL--3.0-blue.svg" alt="License" />
</a>
<a href="https://central.sonatype.com/search?q=continew-starter" target="_blank">
<a href="https://central.sonatype.com/search?q=continew-starter" title="Release" target="_blank">
<img src="https://img.shields.io/maven-central/v/top.continew/continew-starter.svg?label=Maven%20Central&logo=sonatype&logoColor=FFF" alt="Release" />
</a>
<a href="https://app.codacy.com/gh/continew-org/continew-starter/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade" target="_blank">
<img src="https://app.codacy.com/project/badge/Grade/90ed633957a9410aa8745f0654827c01" alt="Codacy Badge" />
<a href="https://spring.io/projects/spring-boot" title="Spring Boot" target="_blank">
<img src="https://img.shields.io/badge/Spring Boot-3.2.12-%236CB52D.svg?logo=Spring-Boot" alt="Spring Boot" />
</a>
<a href="https://sonarcloud.io/summary/new_code?id=Charles7c_continew-starter" target="_blank">
<img src="https://sonarcloud.io/api/project_badges/measure?project=Charles7c_continew-starter&metric=alert_status" alt="Sonar Status" />
</a>
<a href="https://spring.io/projects/spring-boot" target="_blank">
<img src="https://img.shields.io/badge/Spring Boot-3.2.7-%236CB52D.svg?logo=Spring-Boot" alt="Spring Boot" />
</a>
<a href="https://github.com/continew-org/continew-starter" target="_blank">
<a href="https://github.com/continew-org/continew-starter" title="Open JDK" target="_blank">
<img src="https://img.shields.io/badge/Open JDK-17-%236CB52D.svg?logo=OpenJDK&logoColor=FFF" alt="Open JDK" />
</a>
<a href="https://github.com/continew-org/continew-starter" target="_blank">
<img src="https://img.shields.io/github/stars/continew-org/continew-starter?style=social" alt="GitHub stars" />
<a href="https://app.codacy.com/gh/continew-org/continew-starter/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade" title="Codacy" target="_blank">
<img src="https://app.codacy.com/project/badge/Grade/90ed633957a9410aa8745f0654827c01" alt="Codacy" />
</a>
<a href="https://github.com/continew-org/continew-starter" target="_blank">
<img src="https://img.shields.io/github/forks/continew-org/continew-starter?style=social" alt="GitHub forks" />
<a href="https://sonarcloud.io/summary/new_code?id=Charles7c_continew-starter" title="Sonar" target="_blank">
<img src="https://sonarcloud.io/api/project_badges/measure?project=Charles7c_continew-starter&metric=alert_status" alt="Sonar" />
</a>
<a href="https://gitee.com/continew/continew-starter" target="_blank">
<img src="https://gitee.com/continew/continew-starter/badge/star.svg?theme=white" alt="Gitee stars" />
<br />
<a href="https://github.com/continew-org/continew-starter/blob/dev/LICENSE" title="License" target="_blank">
<img src="https://img.shields.io/badge/License-LGPL--3.0-blue.svg" alt="License" />
</a>
<a href="https://gitee.com/continew/continew-starter" target="_blank">
<img src="https://gitee.com/continew/continew-starter/badge/fork.svg?theme=white" alt="Gitee forks" />
<a href="https://github.com/continew-org/continew-starter" title="GitHub Stars" target="_blank">
<img src="https://img.shields.io/github/stars/continew-org/continew-starter?style=social" alt="GitHub Stars" />
</a>
<a href="https://github.com/continew-org/continew-starter" title="GitHub Forks" target="_blank">
<img src="https://img.shields.io/github/forks/continew-org/continew-starter?style=social" alt="GitHub Forks" />
</a>
<a href="https://gitee.com/continew/continew-starter" title="Gitee Stars" target="_blank">
<img src="https://gitee.com/continew/continew-starter/badge/star.svg?theme=white" alt="Gitee Stars" />
</a>
<a href="https://gitee.com/continew/continew-starter" title="Gitee Forks" target="_blank">
<img src="https://gitee.com/continew/continew-starter/badge/fork.svg?theme=white" alt="Gitee Forks" />
</a>
<a href="https://gitcode.com/continew/continew-starter" title="GitCode Stars" target="_blank">
<img src="https://gitcode.com/continew/continew-starter/star/badge.svg" alt="GitCode Stars" />
</a>
## 简介
@@ -173,7 +177,8 @@ continew-starter
│ └─ continew-starter-messaging-websocketWebSocket
├─ continew-starter-log日志模块
│ ├─ continew-starter-log-core通用模块
─ continew-starter-log-interceptor拦截器版Spring Boot Actuator HttpTrace 增强版)
─ continew-starter-log-aop基于 AOP 实现
│ └─ continew-starter-log-interceptor基于拦截器实现Spring Boot Actuator HttpTrace 增强版))
├─ continew-starter-file文件处理模块
│ └─ continew-starter-file-excelEasy Excel
├─ continew-starter-storage存储模块
@@ -224,22 +229,11 @@ ContiNew Starter 的分支目前分为下个大版本的开发分支和上个大
## 反馈交流
💬 欢迎各位小伙伴儿扫描下方二维码加好友,备注 `cnadmin`,拉你进群,探讨技术、提提需求~
加入交流群后,你将会:
- 第一时间收到框架动态
- 第一时间收到框架更新通知
- 第一时间收到框架 Bug 通知
- 和众多大佬互相 (huá shuǐ) 交流 (mō yú)
欢迎各位小伙伴儿扫描下方二维码加入项目交流群,与项目维护团队及其他大佬用户实时交流讨论。
<div align="left">
<img src="https://continew.top/qrcode.jpg" alt="二维码" width="230px" />
<img src=".image/qrcode.jpg" alt="二维码" height="230px" />
</div>
<details>
<summary>无加群意愿</summary>
💬 如无加群意愿,欢迎在 <a href="https://github.com/continew-org/continew-starter/issues" target="_blank">Issues</a> 中反馈交流~ 🍻
</details>
## 鸣谢

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.10</version>
<version>3.2.12</version>
<relativePath/>
</parent>
@@ -43,33 +43,33 @@
<properties>
<!-- 项目版本号 -->
<revision>2.7.5</revision>
<snail-job.version>1.1.2</snail-job.version>
<revision>2.8.0</revision>
<snail-job.version>1.2.0</snail-job.version>
<sa-token.version>1.39.0</sa-token.version>
<just-auth.version>1.16.6</just-auth.version>
<just-auth.version>1.16.7</just-auth.version>
<mybatis-plus.version>3.5.8</mybatis-plus.version>
<mybatis-flex.version>1.9.7</mybatis-flex.version>
<mybatis-flex.version>1.10.3</mybatis-flex.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<p6spy.version>3.9.1</p6spy.version>
<jetcache.version>2.7.6</jetcache.version>
<redisson.version>3.36.0</redisson.version>
<cosid.version>2.9.9</cosid.version>
<jetcache.version>2.7.7</jetcache.version>
<redisson.version>3.41.0</redisson.version>
<cosid.version>2.10.1</cosid.version>
<sms4j.version>3.3.3</sms4j.version>
<aj-captcha.version>1.3.0</aj-captcha.version>
<easy-captcha.version>1.6.2</easy-captcha.version>
<easy-excel.version>3.3.4</easy-excel.version>
<nashorn.version>15.4</nashorn.version>
<nashorn.version>15.5</nashorn.version>
<x-file-storage.version>2.2.1</x-file-storage.version>
<aws-s3.version>1.12.771</aws-s3.version>
<graceful-response.version>5.0.0-boot3</graceful-response.version>
<aws-s3.version>1.12.780</aws-s3.version>
<graceful-response.version>5.0.4-boot3</graceful-response.version>
<crane4j.version>2.9.0</crane4j.version>
<knife4j.version>4.5.0</knife4j.version>
<tlog.version>1.5.2</tlog.version>
<snakeyaml.version>2.3</snakeyaml.version>
<okhttp.version>4.12.0</okhttp.version>
<ttl.version>2.14.5</ttl.version>
<ip2region.version>3.2.6</ip2region.version>
<hutool.version>5.8.32</hutool.version>
<ip2region.version>3.2.12</ip2region.version>
<hutool.version>5.8.34</hutool.version>
<!-- Maven Plugin Versions -->
<flatten.version>1.6.0</flatten.version>
<spotless.version>2.43.0</spotless.version>
@@ -483,13 +483,20 @@
<version>${revision}</version>
</dependency>
<!-- 日志模块 - 拦截器Spring Boot Actuator HttpTrace 增强版) -->
<!-- 日志模块 - 基于拦截器实现Spring Boot Actuator HttpTrace 增强版) -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-log-interceptor</artifactId>
<version>${revision}</version>
</dependency>
<!-- 日志模块 - 基于 AOP 实现 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-log-aop</artifactId>
<version>${revision}</version>
</dependency>
<!-- 日志模块 - 核心模块 -->
<dependency>
<groupId>top.continew</groupId>
@@ -680,7 +687,8 @@
<configuration>
<artifacts>
<artifact>
<file>${project.build.directory}/effective-pom/continew-starter-dependencies.xml</file>
<file>${project.build.directory}/effective-pom/continew-starter-dependencies.xml
</file>
<type>effective-pom</type>
</artifact>
</artifacts>

View File

@@ -93,12 +93,10 @@ public class PageResp<L> extends BasePageResp<L> {
pageResp.setTotal(list.size());
// 对列表数据进行分页
int fromIndex = (page - 1) * size;
int toIndex = page * size + fromIndex;
if (fromIndex > list.size()) {
if (fromIndex >= list.size()) {
pageResp.setList(new ArrayList<>(0));
} else if (toIndex >= list.size()) {
pageResp.setList(list.subList(fromIndex, list.size()));
} else {
int toIndex = Math.min(fromIndex + size, list.size());
pageResp.setList(list.subList(fromIndex, toIndex));
}
return pageResp;

View File

@@ -93,12 +93,10 @@ public class PageResp<L> extends BasePageResp<L> {
pageResp.setTotal(list.size());
// 对列表数据进行分页
int fromIndex = (page - 1) * size;
int toIndex = page * size + fromIndex;
if (fromIndex > list.size()) {
if (fromIndex >= list.size()) {
pageResp.setList(new ArrayList<>(0));
} else if (toIndex >= list.size()) {
pageResp.setList(list.subList(fromIndex, list.size()));
} else {
int toIndex = Math.min(fromIndex + size, list.size());
pageResp.setList(list.subList(fromIndex, toIndex));
}
return pageResp;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.extension.tenant.handler;
package top.continew.starter.extension.tenant;
import top.continew.starter.extension.tenant.config.TenantDataSource;
@@ -31,9 +31,9 @@ public interface TenantDataSourceHandler {
/**
* 切换数据源
*
* @param dataSourceName 数据源名称
* @param tenantDataSource 数据源配置
*/
void changeDataSource(String dataSourceName);
void changeDataSource(TenantDataSource tenantDataSource);
/**
* 是否存在指定数据源

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.tenant;
/**
* 租户处理器
*
* @author 小熊
* @since 2.8.0
*/
public interface TenantHandler {
/**
* 在指定租户中执行
*
* @param tenantId 租户 ID
* @param runnable 方法
*/
void execute(Long tenantId, Runnable runnable);
}

View File

@@ -19,7 +19,7 @@ package top.continew.starter.extension.tenant.annotation;
import java.lang.annotation.*;
/**
* 多租户数据源级隔离忽略注解
* 多租户忽略注解
*
* @author Charles7c
* @since 2.7.0
@@ -27,5 +27,5 @@ import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantDataSourceIgnore {
public @interface TenantIgnore {
}

View File

@@ -16,11 +16,13 @@
package top.continew.starter.extension.tenant.autoconfigure;
import cn.hutool.core.convert.Convert;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import top.continew.starter.extension.tenant.context.TenantContext;
import top.continew.starter.extension.tenant.annotation.TenantIgnore;
import top.continew.starter.extension.tenant.config.TenantProvider;
import top.continew.starter.extension.tenant.context.TenantContextHolder;
/**
@@ -29,20 +31,31 @@ import top.continew.starter.extension.tenant.context.TenantContextHolder;
* @author Charles7c
* @since 2.7.0
*/
public class TenantInterceptor implements HandlerInterceptor {
public class TenantInterceptor implements HandlerInterceptor, Ordered {
private final TenantProperties tenantProperties;
private final TenantProvider tenantProvider;
public TenantInterceptor(TenantProperties tenantProperties) {
public TenantInterceptor(TenantProperties tenantProperties, TenantProvider tenantProvider) {
this.tenantProperties = tenantProperties;
this.tenantProvider = tenantProvider;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (handler instanceof HandlerMethod handlerMethod) {
TenantIgnore tenantIgnore = handlerMethod.getMethodAnnotation(TenantIgnore.class);
if (tenantIgnore != null) {
return true;
}
}
String tenantId = request.getHeader(tenantProperties.getTenantIdHeader());
TenantContext tenantContext = new TenantContext();
tenantContext.setTenantId(Convert.toLong(tenantId));
TenantContextHolder.setContext(tenantContext);
TenantContextHolder.setContext(tenantProvider.getByTenantId(tenantId, true));
return true;
}
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
}

View File

@@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.extension.tenant.config.TenantProvider;
/**
* 多租户 Web MVC 自动配置
@@ -35,13 +36,15 @@ import top.continew.starter.core.constant.PropertiesConstants;
public class TenantWebMvcAutoConfiguration implements WebMvcConfigurer {
private final TenantProperties tenantProperties;
private final TenantProvider tenantProvider;
public TenantWebMvcAutoConfiguration(TenantProperties tenantProperties) {
public TenantWebMvcAutoConfiguration(TenantProperties tenantProperties, TenantProvider tenantProvider) {
this.tenantProperties = tenantProperties;
this.tenantProvider = tenantProvider;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TenantInterceptor(tenantProperties));
registry.addInterceptor(new TenantInterceptor(tenantProperties, tenantProvider));
}
}

View File

@@ -16,19 +16,23 @@
package top.continew.starter.extension.tenant.config;
import top.continew.starter.extension.tenant.context.TenantContext;
/**
* 租户数据源提供者
* 租户提供者
*
* @author Charles7c
* @author 小熊
* @since 2.7.0
*/
public interface TenantDataSourceProvider {
public interface TenantProvider {
/**
* 根据租户 ID 获取数据源配置
* 根据租户 ID 获取租户上下文
*
* @param tenantId 租户 ID
* @return 数据源配置
* @param isVerify 是否验证有效性
* @return 租户上下文
*/
TenantDataSource getByTenantId(String tenantId);
TenantContext getByTenantId(String tenantId, boolean isVerify);
}

View File

@@ -16,6 +16,9 @@
package top.continew.starter.extension.tenant.context;
import top.continew.starter.extension.tenant.config.TenantDataSource;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
/**
* 租户上下文
*
@@ -29,6 +32,16 @@ public class TenantContext {
*/
private Long tenantId;
/**
* 隔离级别
*/
private TenantIsolationLevel isolationLevel;
/**
* 数据源信息
*/
private TenantDataSource dataSource;
public Long getTenantId() {
return tenantId;
}
@@ -36,4 +49,20 @@ public class TenantContext {
public void setTenantId(Long tenantId) {
this.tenantId = tenantId;
}
public TenantIsolationLevel getIsolationLevel() {
return isolationLevel;
}
public void setIsolationLevel(TenantIsolationLevel isolationLevel) {
this.isolationLevel = isolationLevel;
}
public TenantDataSource getDataSource() {
return dataSource;
}
public void setDataSource(TenantDataSource dataSource) {
this.dataSource = dataSource;
}
}

View File

@@ -16,7 +16,11 @@
package top.continew.starter.extension.tenant.context;
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
import top.continew.starter.extension.tenant.config.TenantDataSource;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
import java.util.Optional;
@@ -66,4 +70,24 @@ public class TenantContextHolder {
public static Long getTenantId() {
return Optional.ofNullable(getContext()).map(TenantContext::getTenantId).orElse(null);
}
/**
* 获取租户隔离级别
*
* @return 租户隔离级别
*/
public static TenantIsolationLevel getIsolationLevel() {
return Optional.ofNullable(getContext())
.map(TenantContext::getIsolationLevel)
.orElse(SpringUtil.getBean(TenantProperties.class).getIsolationLevel());
}
/**
* 获取租户数据源
*
* @return 租户数据源
*/
public static TenantDataSource getDataSource() {
return Optional.ofNullable(getContext()).map(TenantContext::getDataSource).orElse(null);
}
}

View File

@@ -23,7 +23,6 @@
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<optional>true</optional>
</dependency>
<!-- 核心模块 -->

View File

@@ -20,6 +20,7 @@ import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
@@ -30,11 +31,17 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean;
import org.springframework.core.ResolvableType;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.extension.tenant.config.TenantDataSourceProvider;
import top.continew.starter.extension.tenant.handler.*;
import top.continew.starter.extension.tenant.config.TenantProvider;
import top.continew.starter.extension.tenant.handler.DefaultTenantHandler;
import top.continew.starter.extension.tenant.TenantDataSourceHandler;
import top.continew.starter.extension.tenant.TenantHandler;
import top.continew.starter.extension.tenant.handler.datasource.DefaultTenantDataSourceHandler;
import top.continew.starter.extension.tenant.handler.datasource.TenantDataSourceAdvisor;
import top.continew.starter.extension.tenant.handler.datasource.TenantDataSourceInterceptor;
import top.continew.starter.extension.tenant.handler.line.DefaultTenantLineHandler;
/**
* 租户自动配置
* 租户自动配置
*
* @author Charles7c
* @since 2.7.0
@@ -45,89 +52,82 @@ import top.continew.starter.extension.tenant.handler.*;
public class TenantAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(TenantAutoConfiguration.class);
private final TenantProperties tenantProperties;
private TenantAutoConfiguration() {
public TenantAutoConfiguration(TenantProperties tenantProperties) {
this.tenantProperties = tenantProperties;
}
/**
* 租户隔离级别:行级
* 租户行级隔离拦截器
*/
@AutoConfiguration
@ConditionalOnProperty(name = PropertiesConstants.TENANT + ".isolation-level", havingValue = "line", matchIfMissing = true)
public static class Line {
static {
log.debug("[ContiNew Starter] - Auto Configuration 'Tenant-Line' completed initialization.");
}
/**
* 租户行级隔离拦截器
*/
@Bean
@ConditionalOnMissingBean
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantLineHandler tenantLineHandler) {
return new TenantLineInnerInterceptor(tenantLineHandler);
}
/**
* 租户行级隔离处理器(默认)
*/
@Bean
@ConditionalOnMissingBean
public TenantLineHandler tenantLineHandler(TenantProperties properties) {
return new DefaultTenantLineHandler(properties);
}
@Bean
@ConditionalOnMissingBean
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantLineHandler tenantLineHandler) {
return new TenantLineInnerInterceptor(tenantLineHandler);
}
/**
* 租户隔离级别:数据源级
* 租户行级隔离处理器(默认)
*/
@AutoConfiguration
@ConditionalOnProperty(name = PropertiesConstants.TENANT + ".isolation-level", havingValue = "datasource")
public static class DataSource {
static {
log.debug("[ContiNew Starter] - Auto Configuration 'Tenant-DataSource' completed initialization.");
}
@Bean
@ConditionalOnMissingBean
public TenantLineHandler tenantLineHandler(TenantProperties properties) {
return new DefaultTenantLineHandler(properties);
}
/**
* 租户数据源级隔离通知
*/
@Bean
@ConditionalOnMissingBean
public TenantDataSourceAdvisor tenantDataSourceAdvisor(TenantDataSourceInterceptor tenantDataSourceInterceptor) {
return new TenantDataSourceAdvisor(tenantDataSourceInterceptor);
}
/**
* 租户数据源级隔离通知
*/
@Bean
@ConditionalOnMissingBean
public TenantDataSourceAdvisor tenantDataSourceAdvisor(TenantDataSourceInterceptor tenantDataSourceInterceptor) {
return new TenantDataSourceAdvisor(tenantDataSourceInterceptor);
}
/**
* 租户数据源级隔离拦截器
*/
@Bean
@ConditionalOnMissingBean
public TenantDataSourceInterceptor tenantDataSourceInterceptor(TenantDataSourceHandler tenantDataSourceHandler) {
return new TenantDataSourceInterceptor(tenantDataSourceHandler);
}
/**
* 租户数据源级隔离拦截器
*/
@Bean
@ConditionalOnMissingBean
public TenantDataSourceInterceptor tenantDataSourceInterceptor(TenantDataSourceHandler tenantDataSourceHandler) {
return new TenantDataSourceInterceptor(tenantDataSourceHandler);
}
/**
* 租户数据源级隔离处理器(默认)
*/
@Bean
@ConditionalOnMissingBean
public TenantDataSourceHandler tenantDataSourceHandler(TenantDataSourceProvider tenantDataSourceProvider,
DynamicRoutingDataSource dynamicRoutingDataSource,
DefaultDataSourceCreator dataSourceCreator) {
return new DefaultTenantDataSourceHandler(tenantDataSourceProvider, dynamicRoutingDataSource, dataSourceCreator);
}
/**
* 租户数据源级隔离处理器(默认)
*/
@Bean
@ConditionalOnMissingBean
public TenantDataSourceHandler tenantDataSourceHandler(javax.sql.DataSource dataSource,
DefaultDataSourceCreator dataSourceCreator) {
return new DefaultTenantDataSourceHandler((DynamicRoutingDataSource)dataSource, dataSourceCreator);
}
/**
* 租户数据源提供者
*/
@Bean
@ConditionalOnMissingBean
public TenantDataSourceProvider tenantDataSourceProvider() {
if (log.isErrorEnabled()) {
log.error("Consider defining a bean of type '{}' in your configuration.", ResolvableType
.forClass(TenantDataSourceProvider.class));
}
throw new NoSuchBeanDefinitionException(TenantDataSourceProvider.class);
/**
* 租户提供者
*/
@Bean
@ConditionalOnMissingBean
public TenantProvider tenantProvider() {
if (log.isErrorEnabled()) {
log.error("Consider defining a bean of type '{}' in your configuration.", ResolvableType
.forClass(TenantProvider.class));
}
throw new NoSuchBeanDefinitionException(TenantProvider.class);
}
/**
* 租户处理器
*/
@Bean
@ConditionalOnMissingBean
public TenantHandler tenantHandler(TenantDataSourceHandler tenantDataSourceHandler, TenantProvider tenantProvider) {
return new DefaultTenantHandler(tenantProperties, tenantDataSourceHandler, tenantProvider);
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Tenant' completed initialization.");
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.tenant.handler;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import top.continew.starter.extension.tenant.TenantDataSourceHandler;
import top.continew.starter.extension.tenant.TenantHandler;
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
import top.continew.starter.extension.tenant.config.TenantProvider;
import top.continew.starter.extension.tenant.context.TenantContext;
import top.continew.starter.extension.tenant.context.TenantContextHolder;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
/**
* 租户处理器
*
* @author 小熊
* @since 2.8.0
*/
public class DefaultTenantHandler implements TenantHandler {
private final TenantProperties tenantProperties;
private final TenantDataSourceHandler dataSourceHandler;
private final TenantProvider tenantProvider;
public DefaultTenantHandler(TenantProperties tenantProperties,
TenantDataSourceHandler dataSourceHandler,
TenantProvider tenantProvider) {
this.tenantProperties = tenantProperties;
this.dataSourceHandler = dataSourceHandler;
this.tenantProvider = tenantProvider;
}
@Override
public void execute(Long tenantId, Runnable runnable) {
if (!tenantProperties.isEnabled()) {
return;
}
TenantContext tenantHandler = tenantProvider.getByTenantId(tenantId.toString(), false);
// 保存当前的租户上下文
TenantContext originalContext = TenantContextHolder.getContext();
boolean isPush = false;
try {
// 设置新的租户上下文
TenantContextHolder.setContext(tenantHandler);
// 切换数据源
if (TenantIsolationLevel.DATASOURCE.equals(tenantHandler.getIsolationLevel())) {
dataSourceHandler.changeDataSource(tenantHandler.getDataSource());
isPush = true;
}
// 执行业务逻辑
runnable.run();
} finally {
// 恢复原始的租户上下文
if (originalContext != null) {
TenantContextHolder.setContext(originalContext);
} else {
TenantContextHolder.clearContext();
}
if (isPush) {
DynamicDataSourceContextHolder.poll();
}
}
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.extension.tenant.handler;
package top.continew.starter.extension.tenant.handler.datasource;
import cn.hutool.core.text.CharSequenceUtil;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
@@ -24,7 +24,7 @@ import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.extension.tenant.config.TenantDataSource;
import top.continew.starter.extension.tenant.config.TenantDataSourceProvider;
import top.continew.starter.extension.tenant.TenantDataSourceHandler;
import javax.sql.DataSource;
@@ -39,24 +39,20 @@ public class DefaultTenantDataSourceHandler implements TenantDataSourceHandler {
private static final Logger log = LoggerFactory.getLogger(DefaultTenantDataSourceHandler.class);
private final DynamicRoutingDataSource dynamicRoutingDataSource;
private final DefaultDataSourceCreator dataSourceCreator;
private final TenantDataSourceProvider tenantDataSourceProvider;
public DefaultTenantDataSourceHandler(TenantDataSourceProvider tenantDataSourceProvider,
DynamicRoutingDataSource dynamicRoutingDataSource,
public DefaultTenantDataSourceHandler(DynamicRoutingDataSource dynamicRoutingDataSource,
DefaultDataSourceCreator dataSourceCreator) {
this.tenantDataSourceProvider = tenantDataSourceProvider;
this.dynamicRoutingDataSource = dynamicRoutingDataSource;
this.dataSourceCreator = dataSourceCreator;
}
@Override
public void changeDataSource(String dataSourceName) {
public void changeDataSource(TenantDataSource tenantDataSource) {
if (tenantDataSource == null) {
return;
}
String dataSourceName = tenantDataSource.getPoolName();
if (!this.containsDataSource(dataSourceName)) {
TenantDataSource tenantDataSource = tenantDataSourceProvider.getByTenantId(dataSourceName);
if (null == tenantDataSource) {
throw new IllegalArgumentException("Data source [%s] configuration not found"
.formatted(dataSourceName));
}
DataSource datasource = this.createDataSource(tenantDataSource);
dynamicRoutingDataSource.addDataSource(dataSourceName, datasource);
log.info("Load data source: {}", dataSourceName);

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.extension.tenant.handler;
package top.continew.starter.extension.tenant.handler.datasource;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Pointcut;
@@ -65,7 +65,10 @@ public class TenantDataSourceAdvisor extends AbstractPointcutAdvisor implements
*/
private Pointcut buildPointcut() {
AspectJExpressionPointcut cut = new AspectJExpressionPointcut();
cut.setExpression("!@annotation(top.continew.starter.extension.tenant.annotation.TenantDataSourceIgnore)");
cut.setExpression("""
execution(* *..controller..*(..))
&& !@annotation(top.continew.starter.extension.tenant.annotation.TenantIgnore)
""");
return new ComposablePointcut((Pointcut)cut);
}
}

View File

@@ -14,12 +14,14 @@
* limitations under the License.
*/
package top.continew.starter.extension.tenant.handler;
package top.continew.starter.extension.tenant.handler.datasource;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import top.continew.starter.extension.tenant.context.TenantContextHolder;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
import top.continew.starter.extension.tenant.TenantDataSourceHandler;
/**
* 租户数据源级隔离拦截器
@@ -37,15 +39,13 @@ public class TenantDataSourceInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Long tenantId = TenantContextHolder.getTenantId();
if (null == tenantId) {
if (TenantIsolationLevel.LINE.equals(TenantContextHolder.getIsolationLevel())) {
return invocation.proceed();
}
// 切换数据源
boolean isPush = false;
try {
String dataSourceName = tenantId.toString();
tenantDataSourceHandler.changeDataSource(dataSourceName);
tenantDataSourceHandler.changeDataSource(TenantContextHolder.getDataSource());
isPush = true;
return invocation.proceed();
} finally {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.extension.tenant.handler;
package top.continew.starter.extension.tenant.handler.line;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
@@ -22,6 +22,7 @@ import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import top.continew.starter.extension.tenant.autoconfigure.TenantProperties;
import top.continew.starter.extension.tenant.context.TenantContextHolder;
import top.continew.starter.extension.tenant.enums.TenantIsolationLevel;
/**
* 默认租户行级隔离处理器
@@ -57,6 +58,9 @@ public class DefaultTenantLineHandler implements TenantLineHandler {
if (null != tenantId && tenantId.equals(tenantProperties.getSuperTenantId())) {
return true;
}
if (TenantIsolationLevel.DATASOURCE.equals(TenantContextHolder.getIsolationLevel())) {
return true;
}
return CollUtil.contains(tenantProperties.getIgnoreTables(), tableName);
}
}

View File

@@ -0,0 +1,22 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-log</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-log-aop</artifactId>
<description>ContiNew Starter 日志模块 - 基于 AOP 实现</description>
<dependencies>
<!-- 日志模块 - 核心模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-log-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,126 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.log.aspect;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import top.continew.starter.log.model.LogProperties;
import java.time.Duration;
import java.time.Instant;
/**
* 访问日志切面
*
* @author echo
* @author Charles7c
* @since 2.8.0
*/
@Aspect
public class AccessLogAspect {
private static final Logger log = LoggerFactory.getLogger(AccessLogAspect.class);
private final LogProperties logProperties;
public AccessLogAspect(LogProperties logProperties) {
this.logProperties = logProperties;
}
/**
* 切点 - 匹配所有控制器层的 GET 请求方法
*/
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void pointcut() {
}
/**
* 切点 - 匹配所有控制器层的 GET 请求方法
*/
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void pointcutGet() {
}
/**
* 切点 - 匹配所有控制器层的 POST 请求方法
*/
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void pointcutPost() {
}
/**
* 切点 - 匹配所有控制器层的 PUT 请求方法
*/
@Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)")
public void pointcutPut() {
}
/**
* 切点 - 匹配所有控制器层的 DELETE 请求方法
*/
@Pointcut("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public void pointcutDelete() {
}
/**
* 切点 - 匹配所有控制器层的 PATCH 请求方法
*/
@Pointcut("@annotation(org.springframework.web.bind.annotation.PatchMapping)")
public void pointcutPatch() {
}
/**
* 打印访问日志
*
* @param joinPoint 切点
* @return 返回结果
* @throws Throwable 异常
*/
@Around("pointcut() || pointcutGet() || pointcutPost() || pointcutPut() || pointcutDelete() || pointcutPatch()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Instant startTime = Instant.now();
// 非 Web 环境不记录
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return joinPoint.proceed();
}
HttpServletRequest request = attributes.getRequest();
HttpServletResponse response = attributes.getResponse();
try {
// 打印请求日志
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
log.info("[{}] {}", request.getMethod(), request.getRequestURI());
}
return joinPoint.proceed();
} finally {
Instant endTime = Instant.now();
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
Duration timeTaken = Duration.between(startTime, endTime);
log.info("[{}] {} {} {}ms", request.getMethod(), request.getRequestURI(), response != null
? response.getStatus()
: "N/A", timeTaken.toMillis());
}
}
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.log.aspect;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.text.CharSequenceUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import top.continew.starter.log.annotation.Log;
import top.continew.starter.log.dao.LogDao;
import top.continew.starter.log.LogHandler;
import top.continew.starter.log.model.LogProperties;
import top.continew.starter.log.model.LogRecord;
import top.continew.starter.web.util.SpringWebUtils;
import java.lang.reflect.Method;
import java.time.Instant;
/**
* 日志切面
*
* @author echo
* @author Charles7c
* @since 2.8.0
*/
@Aspect
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
private final LogProperties logProperties;
private final LogHandler logHandler;
private final LogDao logDao;
public LogAspect(LogProperties logProperties, LogHandler logHandler, LogDao logDao) {
this.logProperties = logProperties;
this.logHandler = logHandler;
this.logDao = logDao;
}
/**
* 切点 - 匹配日志注解 {@link Log}
*/
@Pointcut("@annotation(top.continew.starter.log.annotation.Log)")
public void pointcut() {
}
/**
* 记录日志
*
* @param joinPoint 切点
* @return 返回结果
* @throws Throwable 异常
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Instant startTime = Instant.now();
// 指定规则不记录
HttpServletRequest request = SpringWebUtils.getRequest();
Method targetMethod = this.getMethod(joinPoint);
Class<?> targetClass = joinPoint.getTarget().getClass();
if (!isRequestRecord(targetMethod, targetClass)) {
return joinPoint.proceed();
}
String errorMsg = null;
// 开始记录
LogRecord.Started startedLogRecord = logHandler.start(startTime, request);
try {
// 执行目标方法
return joinPoint.proceed();
} catch (Exception e) {
errorMsg = CharSequenceUtil.sub(e.getMessage(), 0, 2000);
throw e;
} finally {
try {
Instant endTime = Instant.now();
HttpServletResponse response = SpringWebUtils.getResponse();
LogRecord logRecord = logHandler.finish(startedLogRecord, endTime, response, logProperties
.getIncludes(), targetMethod, targetClass);
// 记录异常信息
if (errorMsg != null) {
logRecord.setErrorMsg(errorMsg);
}
logDao.add(logRecord);
} catch (Exception e) {
log.error("Logging http log occurred an error: {}.", e.getMessage(), e);
throw e;
}
}
}
/**
* 是否要记录日志
*
* @param targetMethod 目标方法
* @param targetClass 目标类
* @return true需要记录false不需要记录
*/
private boolean isRequestRecord(Method targetMethod, Class<?> targetClass) {
// 非 Web 环境不记录
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if (attributes == null || attributes.getResponse() == null) {
return false;
}
// 如果接口匹配排除列表,不记录日志
if (logProperties.isMatch(attributes.getRequest().getRequestURI())) {
return false;
}
// 如果接口方法或类上有 @Log 注解,且要求忽略该接口,则不记录日志
Log methodLog = AnnotationUtil.getAnnotation(targetMethod, Log.class);
if (null != methodLog && methodLog.ignore()) {
return false;
}
Log classLog = AnnotationUtil.getAnnotation(targetClass, Log.class);
return null == classLog || !classLog.ignore();
}
/**
* 获取方法
*
* @param joinPoint 切点
* @return 方法
*/
private Method getMethod(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
return signature.getMethod();
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.log.autoconfigure;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.continew.starter.log.annotation.ConditionalOnEnabledLog;
import top.continew.starter.log.aspect.AccessLogAspect;
import top.continew.starter.log.aspect.LogAspect;
import top.continew.starter.log.dao.LogDao;
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
import top.continew.starter.log.handler.AopLogHandler;
import top.continew.starter.log.LogFilter;
import top.continew.starter.log.LogHandler;
import top.continew.starter.log.model.LogProperties;
/**
* 日志自动配置
*
* @author Charles7c
* @author echo
* @since 1.1.0
*/
@Configuration
@ConditionalOnEnabledLog
@EnableConfigurationProperties(LogProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class LogAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(LogAutoConfiguration.class);
private final LogProperties logProperties;
public LogAutoConfiguration(LogProperties logProperties) {
this.logProperties = logProperties;
}
/**
* 日志过滤器
*/
@Bean
@ConditionalOnMissingBean
public LogFilter logFilter() {
return new LogFilter(logProperties);
}
/**
* 日志切面
*
* @param logHandler 日志处理器
* @param logDao 日志持久层接口
* @return {@link LogAspect }
*/
@Bean
@ConditionalOnMissingBean
public LogAspect logAspect(LogHandler logHandler, LogDao logDao) {
return new LogAspect(logProperties, logHandler, logDao);
}
/**
* 访问日志切面
*
* @return {@link LogAspect }
*/
@Bean
@ConditionalOnMissingBean
public AccessLogAspect accessLogAspect() {
return new AccessLogAspect(logProperties);
}
/**
* 日志处理器
*/
@Bean
@ConditionalOnMissingBean
public LogHandler logHandler() {
return new AopLogHandler();
}
/**
* 日志持久层接口
*/
@Bean
@ConditionalOnMissingBean
public LogDao logDao() {
return new DefaultLogDaoImpl();
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Log-AOP' completed initialization.");
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.log.handler;
import cn.hutool.core.text.CharSequenceUtil;
import top.continew.starter.log.AbstractLogHandler;
import top.continew.starter.log.model.LogRecord;
import java.lang.reflect.Method;
/**
* 日志处理器-AOP 版实现
*
* @author Charles7c
* @since 2.8.0
*/
public class AopLogHandler extends AbstractLogHandler {
@Override
public void logDescription(LogRecord logRecord, Method targetMethod) {
super.logDescription(logRecord, targetMethod);
if (CharSequenceUtil.isBlank(logRecord.getDescription())) {
logRecord.setDescription("请在该接口方法上指定日志描述");
}
}
@Override
public void logModule(LogRecord logRecord, Method targetMethod, Class<?> targetClass) {
super.logModule(logRecord, targetMethod, targetClass);
if (CharSequenceUtil.isBlank(logRecord.getModule())) {
logRecord.setModule("请在该接口类上指定所属模块");
}
}
}

View File

@@ -0,0 +1 @@
top.continew.starter.log.autoconfigure.LogAutoConfiguration

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.log;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.text.CharSequenceUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import top.continew.starter.log.annotation.Log;
import top.continew.starter.log.enums.Include;
import top.continew.starter.log.http.servlet.RecordableServletHttpRequest;
import top.continew.starter.log.http.servlet.RecordableServletHttpResponse;
import top.continew.starter.log.model.LogRecord;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
/**
* 日志处理器基类
*
* @author Charles7c
* @since 2.8.0
*/
public abstract class AbstractLogHandler implements LogHandler {
@Override
public LogRecord.Started start(Instant startTime, HttpServletRequest request) {
return LogRecord.start(startTime, new RecordableServletHttpRequest(request));
}
@Override
public LogRecord finish(LogRecord.Started started,
Instant endTime,
HttpServletResponse response,
Set<Include> includes,
Method targetMethod,
Class<?> targetClass) {
Set<Include> includeSet = this.getIncludes(includes, targetMethod, targetClass);
LogRecord logRecord = this.finish(started, endTime, response, includeSet);
// 记录日志描述
if (includeSet.contains(Include.DESCRIPTION)) {
this.logDescription(logRecord, targetMethod);
}
// 记录所属模块
if (includeSet.contains(Include.MODULE)) {
this.logModule(logRecord, targetMethod, targetClass);
}
return logRecord;
}
@Override
public LogRecord finish(LogRecord.Started started,
Instant endTime,
HttpServletResponse response,
Set<Include> includes) {
return started.finish(endTime, new RecordableServletHttpResponse(response, response.getStatus()), includes);
}
/**
* 记录日志描述
*
* @param logRecord 日志记录
* @param targetMethod 目标方法
*/
@Override
public void logDescription(LogRecord logRecord, Method targetMethod) {
Log methodLog = AnnotationUtil.getAnnotation(targetMethod, Log.class);
// 例如:@Log("新增部门") -> 新增部门
if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.value())) {
logRecord.setDescription(methodLog.value());
}
}
/**
* 记录所属模块
*
* @param logRecord 日志记录
* @param targetMethod 目标方法
* @param targetClass 目标类
*/
@Override
public void logModule(LogRecord logRecord, Method targetMethod, Class<?> targetClass) {
Log methodLog = AnnotationUtil.getAnnotation(targetMethod, Log.class);
// 例如:@Log(module = "部门管理") -> 部门管理
// 方法级注解优先级高于类级注解
if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.module())) {
logRecord.setModule(methodLog.module());
return;
}
Log classLog = AnnotationUtil.getAnnotation(targetClass, Log.class);
if (null != classLog && CharSequenceUtil.isNotBlank(classLog.module())) {
logRecord.setModule(classLog.module());
}
}
@Override
public Set<Include> getIncludes(Set<Include> includes, Method targetMethod, Class<?> targetClass) {
Log classLog = AnnotationUtil.getAnnotation(targetClass, Log.class);
Set<Include> includeSet = new HashSet<>(includes);
if (null != classLog) {
this.processInclude(includeSet, classLog);
}
// 方法级注解优先级高于类级注解
Log methodLog = AnnotationUtil.getAnnotation(targetMethod, Log.class);
if (null != methodLog) {
this.processInclude(includeSet, methodLog);
}
return includeSet;
}
/**
* 处理日志包含信息
*
* @param includes 日志包含信息
* @param logAnnotation Log 注解
*/
private void processInclude(Set<Include> includes, Log logAnnotation) {
Include[] includeArr = logAnnotation.includes();
if (includeArr.length > 0) {
includes.addAll(Set.of(includeArr));
}
Include[] excludeArr = logAnnotation.excludes();
if (excludeArr.length > 0) {
includes.removeAll(Set.of(excludeArr));
}
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.log.interceptor.handler;
package top.continew.starter.log;
import cn.hutool.extra.spring.SpringUtil;
import jakarta.servlet.FilterChain;
@@ -28,7 +28,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import top.continew.starter.log.interceptor.autoconfigure.LogProperties;
import top.continew.starter.log.model.LogProperties;
import java.io.IOException;
import java.net.URI;

View File

@@ -0,0 +1,100 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.log;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import top.continew.starter.log.enums.Include;
import top.continew.starter.log.model.LogRecord;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.Set;
/**
* 日志处理器
*
* @author Charles7c
* @since 2.8.0
*/
public interface LogHandler {
/**
* 开始日志记录
*
* @param startTime 开始时间
* @param request 请求对象
* @return 日志记录器
*/
LogRecord.Started start(Instant startTime, HttpServletRequest request);
/**
* 结束日志记录
*
* @param started 开始日志记录器
* @param endTime 结束时间
* @param response 响应对象
* @param includes 包含信息
* @return 日志记录
*/
LogRecord finish(LogRecord.Started started, Instant endTime, HttpServletResponse response, Set<Include> includes);
/**
* 结束日志记录
*
* @param started 开始日志记录器-
* @param endTime 结束时间
* @param response 响应对象
* @param includes 包含信息
* @param targetMethod 目标方法
* @param targetClass 目标类
* @return 日志记录
*/
LogRecord finish(LogRecord.Started started,
Instant endTime,
HttpServletResponse response,
Set<Include> includes,
Method targetMethod,
Class<?> targetClass);
/**
* 记录日志描述
*
* @param logRecord 日志记录
* @param targetMethod 目标方法
*/
void logDescription(LogRecord logRecord, Method targetMethod);
/**
* 记录所属模块
*
* @param logRecord 日志记录
* @param targetMethod 目标方法
* @param targetClass 目标类
*/
void logModule(LogRecord logRecord, Method targetMethod, Class<?> targetClass);
/**
* 获取日志包含信息
*
* @param includes 默认包含信息
* @param targetMethod 目标方法
* @param targetClass 目标类
* @return 日志包含信息
*/
Set<Include> getIncludes(Set<Include> includes, Method targetMethod, Class<?> targetClass);
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.log.interceptor.autoconfigure;
package top.continew.starter.log.annotation;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import top.continew.starter.core.constant.PropertiesConstants;

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package top.continew.starter.log.core.annotation;
package top.continew.starter.log.annotation;
import top.continew.starter.log.core.enums.Include;
import top.continew.starter.log.enums.Include;
import java.lang.annotation.*;

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package top.continew.starter.log.core.dao;
package top.continew.starter.log.dao;
import top.continew.starter.log.core.model.LogRecord;
import top.continew.starter.log.model.LogRecord;
import java.util.Collections;
import java.util.List;

View File

@@ -14,10 +14,10 @@
* limitations under the License.
*/
package top.continew.starter.log.core.dao.impl;
package top.continew.starter.log.dao.impl;
import top.continew.starter.log.core.dao.LogDao;
import top.continew.starter.log.core.model.LogRecord;
import top.continew.starter.log.dao.LogDao;
import top.continew.starter.log.model.LogRecord;
import java.util.LinkedList;
import java.util.List;
@@ -30,7 +30,7 @@ import java.util.List;
* @author Charles7c
* @since 1.1.0
*/
public class LogDaoDefaultImpl implements LogDao {
public class DefaultLogDaoImpl implements LogDao {
/**
* 容量

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.log.core.enums;
package top.continew.starter.log.enums;
import java.util.Collections;
import java.util.LinkedHashSet;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.log.core.model;
package top.continew.starter.log.http;
import java.net.URI;
import java.util.Map;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.log.core.model;
package top.continew.starter.log.http;
import java.util.Map;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.log.interceptor.handler;
package top.continew.starter.log.http.servlet;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
@@ -25,7 +25,7 @@ import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.WebUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.log.core.model.RecordableHttpRequest;
import top.continew.starter.log.http.RecordableHttpRequest;
import java.net.URI;
import java.net.URISyntaxException;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.log.interceptor.handler;
package top.continew.starter.log.http.servlet;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
@@ -22,7 +22,7 @@ import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import top.continew.starter.log.core.model.RecordableHttpResponse;
import top.continew.starter.log.http.RecordableHttpResponse;
import top.continew.starter.web.util.ServletUtils;
import java.util.Map;

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package top.continew.starter.log.interceptor.autoconfigure;
package top.continew.starter.log.model;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.log.core.enums.Include;
import top.continew.starter.log.enums.Include;
import top.continew.starter.web.util.SpringWebUtils;
import java.util.ArrayList;

View File

@@ -14,9 +14,11 @@
* limitations under the License.
*/
package top.continew.starter.log.core.model;
package top.continew.starter.log.model;
import top.continew.starter.log.core.enums.Include;
import top.continew.starter.log.enums.Include;
import top.continew.starter.log.http.RecordableHttpRequest;
import top.continew.starter.log.http.RecordableHttpResponse;
import java.time.Duration;
import java.time.Instant;
@@ -63,6 +65,11 @@ public class LogRecord {
*/
private final Instant timestamp;
/**
* 错误信息
*/
private String errorMsg;
public LogRecord(Instant timestamp, LogRequest request, LogResponse response, Duration timeTaken) {
this.timestamp = timestamp;
this.request = request;
@@ -108,9 +115,9 @@ public class LogRecord {
/**
* 结束日志记录
*
* @param clock 时间
* @param response 响应信息
* @param includes 包含信息
* @param timestamp 结束时间
* @param response 响应信息
* @param includes 包含信息
* @return 日志记录
*/
public LogRecord finish(Instant timestamp, RecordableHttpResponse response, Set<Include> includes) {
@@ -164,4 +171,12 @@ public class LogRecord {
public Instant getTimestamp() {
return timestamp;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
public String getErrorMsg() {
return errorMsg;
}
}

View File

@@ -14,13 +14,14 @@
* limitations under the License.
*/
package top.continew.starter.log.core.model;
package top.continew.starter.log.model;
import cn.hutool.core.text.CharSequenceUtil;
import org.springframework.http.HttpHeaders;
import top.continew.starter.core.util.ExceptionUtils;
import top.continew.starter.core.util.IpUtils;
import top.continew.starter.log.core.enums.Include;
import top.continew.starter.log.enums.Include;
import top.continew.starter.log.http.RecordableHttpRequest;
import top.continew.starter.web.util.ServletUtils;
import java.net.URI;

View File

@@ -14,9 +14,10 @@
* limitations under the License.
*/
package top.continew.starter.log.core.model;
package top.continew.starter.log.model;
import top.continew.starter.log.core.enums.Include;
import top.continew.starter.log.enums.Include;
import top.continew.starter.log.http.RecordableHttpResponse;
import java.util.Map;
import java.util.Set;

View File

@@ -10,7 +10,7 @@
</parent>
<artifactId>continew-starter-log-interceptor</artifactId>
<description>ContiNew Starter 日志模块 - 拦截器Spring Boot Actuator HttpTrace 增强版)</description>
<description>ContiNew Starter 日志模块 - 基于拦截器实现Spring Boot Actuator HttpTrace 增强版)</description>
<dependencies>
<!-- Swagger 注解 -->

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.log.interceptor.autoconfigure;
package top.continew.starter.log.autoconfigure;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
@@ -26,10 +26,14 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.continew.starter.log.core.dao.LogDao;
import top.continew.starter.log.core.dao.impl.LogDaoDefaultImpl;
import top.continew.starter.log.interceptor.handler.LogFilter;
import top.continew.starter.log.interceptor.handler.LogInterceptor;
import top.continew.starter.log.annotation.ConditionalOnEnabledLog;
import top.continew.starter.log.dao.LogDao;
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
import top.continew.starter.log.handler.InterceptorLogHandler;
import top.continew.starter.log.LogFilter;
import top.continew.starter.log.LogHandler;
import top.continew.starter.log.interceptor.LogInterceptor;
import top.continew.starter.log.model.LogProperties;
/**
* 日志自动配置
@@ -52,7 +56,7 @@ public class LogAutoConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor(logDao(), logProperties));
registry.addInterceptor(new LogInterceptor(logProperties, logHandler(), logDao()));
}
/**
@@ -64,17 +68,26 @@ public class LogAutoConfiguration implements WebMvcConfigurer {
return new LogFilter(logProperties);
}
/**
* 日志处理器
*/
@Bean
@ConditionalOnMissingBean
public LogHandler logHandler() {
return new InterceptorLogHandler();
}
/**
* 日志持久层接口
*/
@Bean
@ConditionalOnMissingBean
public LogDao logDao() {
return new LogDaoDefaultImpl();
return new DefaultLogDaoImpl();
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Log' completed initialization.");
log.debug("[ContiNew Starter] - Auto Configuration 'Log-Interceptor' completed initialization.");
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.log.handler;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.text.CharSequenceUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import top.continew.starter.log.AbstractLogHandler;
import top.continew.starter.log.model.LogRecord;
import java.lang.reflect.Method;
/**
* 日志处理器-拦截器版实现
*
* @author Charles7c
* @since 2.8.0
*/
public class InterceptorLogHandler extends AbstractLogHandler {
@Override
public void logDescription(LogRecord logRecord, Method targetMethod) {
super.logDescription(logRecord, targetMethod);
if (CharSequenceUtil.isNotBlank(logRecord.getDescription())) {
return;
}
// 例如:@Operation(summary="新增部门") -> 新增部门
Operation methodOperation = AnnotationUtil.getAnnotation(targetMethod, Operation.class);
if (null != methodOperation) {
logRecord.setDescription(CharSequenceUtil.blankToDefault(methodOperation.summary(), "请在该接口方法上指定日志描述"));
}
}
@Override
public void logModule(LogRecord logRecord, Method targetMethod, Class<?> targetClass) {
super.logModule(logRecord, targetMethod, targetClass);
if (CharSequenceUtil.isNotBlank(logRecord.getModule())) {
return;
}
// 例如:@Tag(name = "部门管理") -> 部门管理
Tag classTag = AnnotationUtil.getAnnotation(targetClass, Tag.class);
if (null != classTag) {
String name = classTag.name();
logRecord.setModule(CharSequenceUtil.blankToDefault(name, "请在该接口类上指定所属模块"));
}
}
}

View File

@@ -14,13 +14,11 @@
* limitations under the License.
*/
package top.continew.starter.log.interceptor.handler;
package top.continew.starter.log.interceptor;
import cn.hutool.core.text.CharSequenceUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
@@ -28,16 +26,15 @@ import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNull;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import top.continew.starter.log.core.annotation.Log;
import top.continew.starter.log.core.dao.LogDao;
import top.continew.starter.log.core.enums.Include;
import top.continew.starter.log.core.model.LogRecord;
import top.continew.starter.log.interceptor.autoconfigure.LogProperties;
import top.continew.starter.log.annotation.Log;
import top.continew.starter.log.model.LogProperties;
import top.continew.starter.log.dao.LogDao;
import top.continew.starter.log.LogHandler;
import top.continew.starter.log.model.LogRecord;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
/**
* 日志拦截器
@@ -48,14 +45,16 @@ import java.util.Set;
public class LogInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(LogInterceptor.class);
private final LogDao logDao;
private final LogProperties logProperties;
private final LogHandler logHandler;
private final LogDao logDao;
private final TransmittableThreadLocal<Instant> timeTtl = new TransmittableThreadLocal<>();
private final TransmittableThreadLocal<LogRecord.Started> logTtl = new TransmittableThreadLocal<>();
public LogInterceptor(LogDao logDao, LogProperties logProperties) {
this.logDao = logDao;
public LogInterceptor(LogProperties logProperties, LogHandler logHandler, LogDao logDao) {
this.logProperties = logProperties;
this.logHandler = logHandler;
this.logDao = logDao;
}
@Override
@@ -67,8 +66,9 @@ public class LogInterceptor implements HandlerInterceptor {
log.info("[{}] {}", request.getMethod(), request.getRequestURI());
timeTtl.set(startTime);
}
// 开始日志记录
if (this.isRequestRecord(handler, request)) {
LogRecord.Started startedLogRecord = LogRecord.start(startTime, new RecordableServletHttpRequest(request));
LogRecord.Started startedLogRecord = logHandler.start(startTime, request);
logTtl.set(startedLogRecord);
}
return true;
@@ -90,110 +90,22 @@ public class LogInterceptor implements HandlerInterceptor {
if (null == startedLogRecord) {
return;
}
// 结束日志记录
HandlerMethod handlerMethod = (HandlerMethod)handler;
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
Set<Include> includeSet = this.getIncludes(methodLog, classLog);
LogRecord finishedLogRecord = startedLogRecord
.finish(endTime, new RecordableServletHttpResponse(response, response.getStatus()), includeSet);
// 记录日志描述
if (includeSet.contains(Include.DESCRIPTION)) {
this.logDescription(finishedLogRecord, methodLog, handlerMethod);
}
// 记录所属模块
if (includeSet.contains(Include.MODULE)) {
this.logModule(finishedLogRecord, methodLog, classLog, handlerMethod);
}
logDao.add(finishedLogRecord);
Method targetMethod = handlerMethod.getMethod();
Class<?> targetClass = handlerMethod.getBeanType();
LogRecord logRecord = logHandler.finish(startedLogRecord, endTime, response, logProperties
.getIncludes(), targetMethod, targetClass);
logDao.add(logRecord);
} catch (Exception ex) {
log.error("Logging http log occurred an error: {}.", ex.getMessage(), ex);
throw ex;
} finally {
timeTtl.remove();
logTtl.remove();
}
}
/**
* 获取日志包含信息
*
* @param methodLog 方法级 Log 注解
* @param classLog 类级 Log 注解
* @return 日志包含信息
*/
private Set<Include> getIncludes(Log methodLog, Log classLog) {
Set<Include> includeSet = new HashSet<>(logProperties.getIncludes());
if (null != classLog) {
this.processInclude(includeSet, classLog);
}
if (null != methodLog) {
this.processInclude(includeSet, methodLog);
}
return includeSet;
}
/**
* 处理日志包含信息
*
* @param includes 日志包含信息
* @param logAnnotation Log 注解
*/
private void processInclude(Set<Include> includes, Log logAnnotation) {
Include[] includeArr = logAnnotation.includes();
if (includeArr.length > 0) {
includes.addAll(Set.of(includeArr));
}
Include[] excludeArr = logAnnotation.excludes();
if (excludeArr.length > 0) {
includes.removeAll(Set.of(excludeArr));
}
}
/**
* 记录描述
*
* @param logRecord 日志信息
* @param methodLog 方法级 Log 注解
* @param handlerMethod 处理器方法
*/
private void logDescription(LogRecord logRecord, Log methodLog, HandlerMethod handlerMethod) {
// 例如@Log("新增部门") -> 新增部门
if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.value())) {
logRecord.setDescription(methodLog.value());
return;
}
// 例如@Operation(summary="新增部门") -> 新增部门
Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
if (null != methodOperation) {
logRecord.setDescription(CharSequenceUtil.blankToDefault(methodOperation.summary(), "请在该接口方法上指定日志描述"));
}
}
/**
* 记录模块
*
* @param logRecord 日志信息
* @param methodLog 方法级 Log 注解
* @param classLog 类级 Log 注解
* @param handlerMethod 处理器方法
*/
private void logModule(LogRecord logRecord, Log methodLog, Log classLog, HandlerMethod handlerMethod) {
// 例如@Log(module = "部门管理") -> 部门管理
if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.module())) {
logRecord.setModule(methodLog.module());
return;
}
if (null != classLog && CharSequenceUtil.isNotBlank(classLog.module())) {
logRecord.setModule(classLog.module());
return;
}
// 例如@Tag(name = "部门管理") -> 部门管理
Tag classTag = handlerMethod.getBeanType().getDeclaredAnnotation(Tag.class);
if (null != classTag) {
String name = classTag.name();
logRecord.setModule(CharSequenceUtil.blankToDefault(name, "请在该接口类上指定所属模块"));
}
}
/**
* 是否要记录日志
*

View File

@@ -1 +1 @@
top.continew.starter.log.interceptor.autoconfigure.LogAutoConfiguration
top.continew.starter.log.autoconfigure.LogAutoConfiguration

View File

@@ -16,6 +16,7 @@
<modules>
<module>continew-starter-log-core</module>
<module>continew-starter-log-interceptor</module>
<module>continew-starter-log-aop</module>
</modules>
<dependencies>

View File

@@ -18,9 +18,10 @@ package top.continew.starter.web.autoconfigure.response;
import com.feiniaojin.gracefulresponse.advice.lifecycle.exception.BeforeControllerAdviceProcess;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.web.util.SpringWebUtils;
import org.springframework.lang.Nullable;
/**
* 默认回调处理器实现
@@ -38,10 +39,9 @@ public class DefaultBeforeControllerAdviceProcessImpl implements BeforeControlle
}
@Override
public void call(Throwable throwable) {
public void call(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception e) {
if (globalResponseProperties.isPrintExceptionInGlobalAdvice()) {
HttpServletRequest request = SpringWebUtils.getRequest();
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), throwable);
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
}
}
}

View File

@@ -20,7 +20,7 @@ import com.feiniaojin.gracefulresponse.ExceptionAliasRegister;
import com.feiniaojin.gracefulresponse.advice.*;
import com.feiniaojin.gracefulresponse.advice.lifecycle.exception.BeforeControllerAdviceProcess;
import com.feiniaojin.gracefulresponse.advice.lifecycle.exception.ControllerAdvicePredicate;
import com.feiniaojin.gracefulresponse.advice.lifecycle.response.ResponseBodyAdvicePredicate;
import com.feiniaojin.gracefulresponse.advice.lifecycle.exception.RejectStrategy;
import com.feiniaojin.gracefulresponse.api.ResponseFactory;
import com.feiniaojin.gracefulresponse.api.ResponseStatusFactory;
import com.feiniaojin.gracefulresponse.defaults.DefaultResponseFactory;
@@ -36,8 +36,10 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.util.GeneralPropertySourceFactory;
@@ -68,12 +70,7 @@ public class GlobalResponseAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GrNotVoidResponseBodyAdvice grNotVoidResponseBodyAdvice() {
GrNotVoidResponseBodyAdvice notVoidResponseBodyAdvice = new GrNotVoidResponseBodyAdvice();
CopyOnWriteArrayList<ResponseBodyAdvicePredicate> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add(notVoidResponseBodyAdvice);
notVoidResponseBodyAdvice.setPredicates(copyOnWriteArrayList);
notVoidResponseBodyAdvice.setResponseBodyAdviceProcessor(notVoidResponseBodyAdvice);
return notVoidResponseBodyAdvice;
return new GrNotVoidResponseBodyAdvice();
}
/**
@@ -82,12 +79,7 @@ public class GlobalResponseAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GrVoidResponseBodyAdvice grVoidResponseBodyAdvice() {
GrVoidResponseBodyAdvice voidResponseBodyAdvice = new GrVoidResponseBodyAdvice();
CopyOnWriteArrayList<ResponseBodyAdvicePredicate> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add(voidResponseBodyAdvice);
voidResponseBodyAdvice.setPredicates(copyOnWriteArrayList);
voidResponseBodyAdvice.setResponseBodyAdviceProcessor(voidResponseBodyAdvice);
return voidResponseBodyAdvice;
return new GrVoidResponseBodyAdvice();
}
/**
@@ -103,9 +95,10 @@ public class GlobalResponseAutoConfiguration {
* 框架异常处理器
*/
@Bean
public FrameworkExceptionAdvice frameworkExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess) {
public FrameworkExceptionAdvice frameworkExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess,
@Lazy RejectStrategy rejectStrategy) {
FrameworkExceptionAdvice frameworkExceptionAdvice = new FrameworkExceptionAdvice();
frameworkExceptionAdvice.setRejectStrategy(new DefaultRejectStrategyImpl());
frameworkExceptionAdvice.setRejectStrategy(rejectStrategy);
frameworkExceptionAdvice.setControllerAdviceProcessor(frameworkExceptionAdvice);
frameworkExceptionAdvice.setBeforeControllerAdviceProcess(beforeControllerAdviceProcess);
frameworkExceptionAdvice.setControllerAdviceHttpProcessor(frameworkExceptionAdvice);
@@ -116,9 +109,10 @@ public class GlobalResponseAutoConfiguration {
* 数据校验异常处理器
*/
@Bean
public DataExceptionAdvice dataExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess) {
public DataExceptionAdvice dataExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess,
@Lazy RejectStrategy rejectStrategy) {
DataExceptionAdvice dataExceptionAdvice = new DataExceptionAdvice();
dataExceptionAdvice.setRejectStrategy(new DefaultRejectStrategyImpl());
dataExceptionAdvice.setRejectStrategy(rejectStrategy);
dataExceptionAdvice.setControllerAdviceProcessor(dataExceptionAdvice);
dataExceptionAdvice.setBeforeControllerAdviceProcess(beforeControllerAdviceProcess);
dataExceptionAdvice.setControllerAdviceHttpProcessor(dataExceptionAdvice);
@@ -129,9 +123,10 @@ public class GlobalResponseAutoConfiguration {
* 默认全局异常处理器
*/
@Bean
public DefaultGlobalExceptionAdvice defaultGlobalExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess) {
public DefaultGlobalExceptionAdvice defaultGlobalExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess,
@Lazy RejectStrategy rejectStrategy) {
DefaultGlobalExceptionAdvice advice = new DefaultGlobalExceptionAdvice();
advice.setRejectStrategy(new DefaultRejectStrategyImpl());
advice.setRejectStrategy(rejectStrategy);
CopyOnWriteArrayList<ControllerAdvicePredicate> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add(advice);
advice.setPredicates(copyOnWriteArrayList);
@@ -145,27 +140,40 @@ public class GlobalResponseAutoConfiguration {
* 默认参数校验异常处理器
*/
@Bean
public DefaultValidationExceptionAdvice defaultValidationExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess) {
public DefaultValidationExceptionAdvice defaultValidationExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess,
@Lazy RejectStrategy rejectStrategy) {
DefaultValidationExceptionAdvice advice = new DefaultValidationExceptionAdvice();
advice.setRejectStrategy(new DefaultRejectStrategyImpl());
advice.setRejectStrategy(rejectStrategy);
advice.setControllerAdviceProcessor(advice);
advice.setBeforeControllerAdviceProcess(beforeControllerAdviceProcess);
// 设置默认参数校验异常http处理器
advice.setControllerAdviceHttpProcessor(advice);
return advice;
}
/**
* 拒绝策略
*/
@Bean
public RejectStrategy rejectStrategy() {
return new DefaultRejectStrategyImpl();
}
/**
* 释放异常处理器
*/
@Bean
public ExceptionHandlerExceptionResolver releaseExceptionHandlerExceptionResolver() {
return new ReleaseExceptionHandlerExceptionResolver();
}
/**
* 国际化支持
*/
@Bean
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_RESPONSE, name = "i18n", havingValue = "true")
public GrI18nResponseBodyAdvice grI18nResponseBodyAdvice() {
GrI18nResponseBodyAdvice i18nResponseBodyAdvice = new GrI18nResponseBodyAdvice();
CopyOnWriteArrayList<ResponseBodyAdvicePredicate> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add(i18nResponseBodyAdvice);
i18nResponseBodyAdvice.setPredicates(copyOnWriteArrayList);
i18nResponseBodyAdvice.setResponseBodyAdviceProcessor(i18nResponseBodyAdvice);
return i18nResponseBodyAdvice;
return new GrI18nResponseBodyAdvice();
}
/**