通用模块工具箱(开箱即用版)
- 通用模块工具箱(开箱即用版)
- 一、基础核心模块(项目初始化必用,100%通用)
-
- [1. 全局配置类](#1. 全局配置类)
-
- [1.1 GlobalCorsConfig(跨域配置)](#1.1 GlobalCorsConfig(跨域配置))
- [1.2 MyBatisPlusConfig(MyBatisPlus配置)](#1.2 MyBatisPlusConfig(MyBatisPlus配置))
- [1.3 RedisConfig(Redis配置)](#1.3 RedisConfig(Redis配置))
- [1.4 SecurityConfig(基础安全配置)](#1.4 SecurityConfig(基础安全配置))
- [1.5 ThreadPoolConfig(通用线程池配置)](#1.5 ThreadPoolConfig(通用线程池配置))
- [2. 核心枚举类](#2. 核心枚举类)
-
- [2.1 ErrorCodeEnum(错误码枚举)](#2.1 ErrorCodeEnum(错误码枚举))
- [2.2 HttpStatusEnum(HTTP状态码映射)](#2.2 HttpStatusEnum(HTTP状态码映射))
- [2.3 BusinessStatusEnum(通用业务状态)](#2.3 BusinessStatusEnum(通用业务状态))
- [3. 基础工具类](#3. 基础工具类)
-
- [3.1 StringUtil(字符串工具)](#3.1 StringUtil(字符串工具))
- [3.2 DateUtil(时间工具)](#3.2 DateUtil(时间工具))
- [3.3 EncryptUtil(加密工具)](#3.3 EncryptUtil(加密工具))
- [3.4 NumberUtil(数字工具)](#3.4 NumberUtil(数字工具))
- [3.5 CollectionUtil(集合工具)](#3.5 CollectionUtil(集合工具))
- [3.6 BeanUtil(对象转换)](#3.6 BeanUtil(对象转换))
- 二、接口交互模块(接口开发必用,100%通用)
-
- [1. 统一返回相关](#1. 统一返回相关)
-
- [1.1 ApiResponse<T>(全局返回体)](#1.1 ApiResponse<T>(全局返回体))
- [1.2 PageParam(分页请求参数)](#1.2 PageParam(分页请求参数))
- [1.3 PageResult<T>(分页结果)](#1.3 PageResult<T>(分页结果))
- [1.4 ApiResultCode(返回码枚举)](#1.4 ApiResultCode(返回码枚举))
- [2. 参数校验相关](#2. 参数校验相关)
-
- [2.1 GroupValidator(分组校验接口)](#2.1 GroupValidator(分组校验接口))
- [2.2 CustomValidAnnotations(自定义校验注解)](#2.2 CustomValidAnnotations(自定义校验注解))
- [2.3 ValidUtil(手动校验工具)](#2.3 ValidUtil(手动校验工具))
- 三、业务逻辑模块(业务开发必用,95%通用)
-
- [1. 异常处理](#1. 异常处理)
-
- [1.1 GlobalExceptionHandler(全局异常拦截)](#1.1 GlobalExceptionHandler(全局异常拦截))
- [1.2 BusinessException(业务异常)](#1.2 BusinessException(业务异常))
- [1.3 SystemException(系统异常)](#1.3 SystemException(系统异常))
- [1.4 ParamInvalidException(参数异常)](#1.4 ParamInvalidException(参数异常))
- [2. 业务工具类](#2. 业务工具类)
-
- [2.1 ExcelUtil(EasyExcel导入导出)](#2.1 ExcelUtil(EasyExcel导入导出))
- [2.2 IdGeneratorUtil(分布式ID生成)](#2.2 IdGeneratorUtil(分布式ID生成))
- [2.3 FileUtil(文件操作工具)](#2.3 FileUtil(文件操作工具))
- [2.4 MoneyUtil(金额计算工具)](#2.4 MoneyUtil(金额计算工具))
- [2.5 CacheUtil(本地缓存工具)](#2.5 CacheUtil(本地缓存工具))
- 四、安全防护模块(上线前必用,95%通用)
-
- [1. 安全工具类](#1. 安全工具类)
-
- [1.1 XssFilter(防XSS攻击过滤器)](#1.1 XssFilter(防XSS攻击过滤器))
- [1.2 SensitiveUtil(敏感信息脱敏工具)](#1.2 SensitiveUtil(敏感信息脱敏工具))
- [1.3 TokenUtil(JWT工具类)](#1.3 TokenUtil(JWT工具类))
- [1.4 SignUtil(接口签名工具)](#1.4 SignUtil(接口签名工具))
- [2. 防护组件](#2. 防护组件)
-
- [2.1 IdempotentAOP(幂等性注解AOP)](#2.1 IdempotentAOP(幂等性注解AOP))
- [2.2 AntiBrushInterceptor(接口防刷拦截器)](#2.2 AntiBrushInterceptor(接口防刷拦截器))
- [2.3 SqlInjectFilter(防SQL注入过滤器)](#2.3 SqlInjectFilter(防SQL注入过滤器))
- 五、监控审计模块(上线前必用,90%通用)
-
- [1. 日志审计](#1. 日志审计)
-
- [1.1 OperateLogAOP(操作日志AOP)](#1.1 OperateLogAOP(操作日志AOP))
- [1.2 TraceIdUtil(全链路TraceId工具)](#1.2 TraceIdUtil(全链路TraceId工具))
- [1.3 LogFormatter(日志格式化工具)](#1.3 LogFormatter(日志格式化工具))
- [2. 监控工具](#2. 监控工具)
-
- [2.1 MetricsUtil(自定义指标工具)](#2.1 MetricsUtil(自定义指标工具))
- [2.2 HealthCheckUtil(健康检查工具)](#2.2 HealthCheckUtil(健康检查工具))
- [2.3 ExceptionLogUtil(异常日志工具)](#2.3 ExceptionLogUtil(异常日志工具))
- 大厂规范
通用模块工具箱(开箱即用版)
以下是符合大厂标准的工具类完整实现,包含包名规范、核心逻辑、详细注释,并标注「常用度」和「所属层级」(控制层/业务层/DAO层/通用层/配置层):
一、基础核心模块(项目初始化必用,100%通用)
1. 全局配置类
1.1 GlobalCorsConfig(跨域配置)
java
package com.xxx.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
/**
* 全局跨域配置(大厂标准:支持多域名、Credentials、自定义请求头)
* 常用度:100%
* 所属层级:配置层(通用)
*
* @author xxx
* @date 2025-11-30
*/
@Configuration
public class GlobalCorsConfig implements WebMvcConfigurer {
/**
* 允许跨域的域名列表(建议从配置文件读取)
*/
private static final List<String> ALLOWED_ORIGINS = Arrays.asList(
"https://www.xxx.com",
"https://test.xxx.com",
"http://localhost:8080"
);
/**
* 基于Filter的跨域配置(兼容所有请求类型)
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许指定域名跨域
config.setAllowedOrigins(ALLOWED_ORIGINS);
// 允许携带Cookie
config.setAllowCredentials(true);
// 允许所有请求方法
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
// 允许所有请求头
config.setAllowedHeaders(Arrays.asList("*"));
// 暴露自定义响应头(如Token)
config.setExposedHeaders(Arrays.asList("X-Token", "Trace-Id"));
// 预检请求有效期(秒)
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 所有接口生效
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
/**
* 基于WebMvc的跨域配置(兜底)
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(ALLOWED_ORIGINS.toArray(new String[0]))
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
1.2 MyBatisPlusConfig(MyBatisPlus配置)
java
package com.xxx.common.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
/**
* MyBatisPlus核心配置(分页+字段自动填充,兼容3.5.x+,适配MySQL/Oracle)
* 常用度:100%
* 所属层级:DAO层(配置)
*
* @author xxx
* @date 2025-11-30
*/
@Configuration
public class MyBatisPlusConfig implements MetaObjectHandler {
/**
* 分页插件(适配多数据库)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// MySQL分页
PaginationInnerInterceptor mysqlPageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
// 溢出总页数后是否进行处理(true:返回最后一页,false:返回空)
mysqlPageInterceptor.setOverflow(true);
// 单页最大条数限制(防止恶意分页)
mysqlPageInterceptor.setMaxLimit(1000L);
interceptor.addInnerInterceptor(mysqlPageInterceptor);
// Oracle分页(按需开启)
// PaginationInnerInterceptor oraclePageInterceptor = new PaginationInnerInterceptor(DbType.ORACLE);
// interceptor.addInnerInterceptor(oraclePageInterceptor);
return interceptor;
}
/**
* 新增时自动填充创建时间/更新时间
*/
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
/**
* 更新时自动填充更新时间
*/
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
1.3 RedisConfig(Redis配置)
java
package com.xxx.common.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis核心配置(Jackson序列化防乱码、连接池优化)
* 常用度:100%
* 所属层级:通用层(配置)
*
* @author xxx
* @date 2025-11-30
*/
@Configuration
public class RedisConfig {
/**
* 自定义RedisTemplate(解决序列化乱码问题)
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// Jackson序列化器(支持LocalDateTime等JDK8时间类型)
Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
// 开启字段可见性
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 开启多类型序列化(避免反序列化类型丢失)
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
// 注册JDK8时间模块
objectMapper.registerModule(new JavaTimeModule());
jacksonSerializer.setObjectMapper(objectMapper);
// String序列化器(Key/HashKey)
StringRedisSerializer stringSerializer = new StringRedisSerializer();
// 配置序列化规则
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(jacksonSerializer);
redisTemplate.setHashValueSerializer(jacksonSerializer);
// 初始化
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 配置Lettuce连接池(默认已集成,此处为自定义参数示例)
*/
@Bean
public LettuceConnectionFactory lettuceConnectionFactory() {
// 实际项目中建议从配置文件读取Redis连接信息
// RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("127.0.0.1", 6379);
// config.setPassword(RedisPassword.of("xxx"));
// return new LettuceConnectionFactory(config);
return (LettuceConnectionFactory) connectionFactory;
}
}
1.4 SecurityConfig(基础安全配置)
java
package com.xxx.common.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
/**
* SpringSecurity基础配置(忽略Swagger/Actuator、关闭CSRF)
* 常用度:100%
* 所属层级:安全层(配置)
*
* @author xxx
* @date 2025-11-30
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* 核心安全过滤链
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 关闭CSRF(前后端分离场景)
.csrf(csrf -> csrf.disable())
// 忽略无需认证的接口
.authorizeHttpRequests(auth -> auth
// Swagger接口
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
// 健康检查接口
.requestMatchers("/actuator/**").permitAll()
// 公开接口
.requestMatchers("/api/v1/public/**").permitAll()
// 其余接口需认证(可根据业务调整)
.anyRequest().authenticated()
);
return http.build();
}
}
1.5 ThreadPoolConfig(通用线程池配置)
java
package com.xxx.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.concurrent.*;
/**
* 通用线程池配置(防OOM、可配置核心/最大线程数)
* 常用度:100%
* 所属层级:通用层(配置)
*
* @author xxx
* @date 2025-11-30
*/
@Configuration
@EnableAsync
public class ThreadPoolConfig {
/**
* 核心线程数(CPU核心数+1)
*/
private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() + 1;
/**
* 最大线程数(CPU核心数*2)
*/
private static final int MAX_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2;
/**
* 队列容量
*/
private static final int QUEUE_CAPACITY = 1000;
/**
* 空闲线程存活时间(秒)
*/
private static final int KEEP_ALIVE_TIME = 60;
/**
* 通用业务线程池
*/
@Bean("businessThreadPool")
public Executor businessThreadPool() {
return new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(QUEUE_CAPACITY),
// 自定义线程名称(便于排查问题)
new ThreadFactory() {
private int count = 1;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "business-thread-" + count++);
}
},
// 拒绝策略(大厂推荐:主线程执行,避免任务丢失)
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
/**
* IO密集型线程池(如文件上传/下载)
*/
@Bean("ioThreadPool")
public Executor ioThreadPool() {
// IO密集型任务线程数可配置为CPU核心数*4
int ioCoreSize = Runtime.getRuntime().availableProcessors() * 4;
int ioMaxSize = Runtime.getRuntime().availableProcessors() * 8;
return new ThreadPoolExecutor(
ioCoreSize,
ioMaxSize,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000),
new ThreadFactory() {
private int count = 1;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "io-thread-" + count++);
}
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
2. 核心枚举类
2.1 ErrorCodeEnum(错误码枚举)
java
package com.xxx.common.enums;
import lombok.Getter;
/**
* 全局错误码枚举(大厂规范:分段管理)
* 常用度:100%
* 所属层级:通用层
*
* @author xxx
* @date 2025-11-30
*/
@Getter
public enum ErrorCodeEnum {
// 系统错误(100-199)
SYSTEM_ERROR(100, "系统异常,请稍后重试"),
SYSTEM_TIMEOUT(101, "系统超时,请稍后重试"),
SERVICE_UNAVAILABLE(102, "服务暂不可用"),
// 认证授权错误(200-299)
TOKEN_INVALID(200, "Token无效或已过期"),
TOKEN_MISSING(201, "Token缺失"),
PERMISSION_DENIED(202, "权限不足"),
USER_NOT_LOGIN(203, "用户未登录"),
// 业务错误(600-999)
BUSINESS_COMMON_ERROR(600, "业务异常"),
DATA_NOT_FOUND(601, "数据不存在"),
DATA_DUPLICATE(602, "数据已存在"),
PARAM_INVALID(603, "参数无效"),
// 扩展业务错误(按模块细分)
USER_NOT_EXIST(604, "用户不存在"),
ORDER_STATUS_ERROR(605, "订单状态异常"),
INSUFFICIENT_BALANCE(606, "余额不足");
/**
* 错误码
*/
private final int code;
/**
* 错误描述
*/
private final String msg;
ErrorCodeEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
/**
* 根据错误码获取枚举
*/
public static ErrorCodeEnum getByCode(int code) {
for (ErrorCodeEnum e : values()) {
if (e.getCode() == code) {
return e;
}
}
return SYSTEM_ERROR;
}
}
2.2 HttpStatusEnum(HTTP状态码映射)
java
package com.xxx.common.enums;
import lombok.Getter;
import org.springframework.http.HttpStatus;
/**
* HTTP状态码枚举(与业务错误码关联)
* 常用度:100%
* 所属层级:通用层
*
* @author xxx
* @date 2025-11-30
*/
@Getter
public enum HttpStatusEnum {
SUCCESS(HttpStatus.OK.value(), "成功"),
BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), "请求参数错误"),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED.value(), "未认证"),
FORBIDDEN(HttpStatus.FORBIDDEN.value(), "禁止访问"),
NOT_FOUND(HttpStatus.NOT_FOUND.value(), "资源不存在"),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误"),
TOO_MANY_REQUESTS(HttpStatus.TOO_MANY_REQUESTS.value(), "请求过于频繁");
/**
* HTTP状态码
*/
private final int code;
/**
* 描述
*/
private final String msg;
HttpStatusEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
/**
* 根据业务错误码匹配HTTP状态码
*/
public static HttpStatusEnum matchByBizCode(ErrorCodeEnum bizCode) {
switch (bizCode) {
case PARAM_INVALID:
return BAD_REQUEST;
case TOKEN_INVALID:
case TOKEN_MISSING:
case USER_NOT_LOGIN:
return UNAUTHORIZED;
case PERMISSION_DENIED:
return FORBIDDEN;
case DATA_NOT_FOUND:
return NOT_FOUND;
case SYSTEM_ERROR:
case SYSTEM_TIMEOUT:
case SERVICE_UNAVAILABLE:
return INTERNAL_SERVER_ERROR;
default:
return SUCCESS;
}
}
}
2.3 BusinessStatusEnum(通用业务状态)
java
package com.xxx.common.enums;
import lombok.Getter;
/**
* 通用业务状态枚举(大厂规范:统一状态值)
* 常用度:100%
* 所属层级:业务层
*
* @author xxx
* @date 2025-11-30
*/
@Getter
public enum BusinessStatusEnum {
// 启用/禁用
ENABLE(1, "启用"),
DISABLE(0, "禁用"),
// 有效/无效
VALID(1, "有效"),
INVALID(0, "无效"),
// 审核状态
AUDIT_PENDING(0, "待审核"),
AUDIT_APPROVED(1, "审核通过"),
AUDIT_REJECTED(2, "审核驳回"),
// 支付状态
PAY_PENDING(0, "待支付"),
PAY_SUCCESS(1, "支付成功"),
PAY_FAILED(2, "支付失败"),
PAY_REFUNDED(3, "已退款");
/**
* 状态值
*/
private final int value;
/**
* 状态描述
*/
private final String desc;
BusinessStatusEnum(int value, String desc) {
this.value = value;
this.desc = desc;
}
/**
* 根据值获取枚举
*/
public static BusinessStatusEnum getByValue(int value) {
for (BusinessStatusEnum e : values()) {
if (e.getValue() == value) {
return e;
}
}
return null;
}
/**
* 判断是否为启用状态
*/
public boolean isEnable() {
return this == ENABLE;
}
/**
* 判断是否为有效状态
*/
public boolean isValid() {
return this == VALID;
}
}
```##### 2.4 EnvEnum(环境枚举)
```java
package com.xxx.common.enums;
import lombok.Getter;
/**
* 环境枚举(避免硬编码环境判断)
* 常用度:100%
* 所属层级:通用层
*
* @author xxx
* @date 2025-11-30
*/
@Getter
public enum EnvEnum {
DEV("dev", "开发环境"),
TEST("test", "测试环境"),
PRE("pre", "预发布环境"),
PROD("prod", "生产环境");
/**
* 环境标识
*/
private final String code;
/**
* 环境描述
*/
private final String desc;
EnvEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
/**
* 根据标识获取枚举
*/
public static EnvEnum getByCode(String code) {
for (EnvEnum e : values()) {
if (e.getCode().equals(code)) {
return e;
}
}
return DEV;
}
/**
* 判断是否为生产环境
*/
public boolean isProd() {
return this == PROD;
}
/**
* 判断是否为开发/测试环境
*/
public boolean isDevOrTest() {
return this == DEV || this == TEST;
}
}
3. 基础工具类
3.1 StringUtil(字符串工具)
java
package com.xxx.common.util;
import org.apache.commons.lang3.StringUtils;
import java.util.regex.Pattern;
/**
* 字符串工具类(空判断/脱敏/格式校验,避免NPE)
* 常用度:100%
* 所属层级:通用层(控制/业务/DAO)
*
* @author xxx
* @date 2025-11-30
*/
public class StringUtil extends StringUtils {
/**
* 手机号正则
*/
private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
/**
* 身份证正则(18位)
*/
private static final Pattern ID_CARD_PATTERN = Pattern.compile("^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$");
/**
* 手机号脱敏(138****1234)
*/
public static String maskMobile(String mobile) {
if (isEmpty(mobile) || !isMobile(mobile)) {
return mobile;
}
return mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
/**
* 身份证脱敏(110101********1234)
*/
public static String maskIdCard(String idCard) {
if (isEmpty(idCard) || idCard.length() != 18) {
return idCard;
}
return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
}
/**
* 姓名脱敏(张*三 / 李*)
*/
public static String maskName(String name) {
if (isEmpty(name)) {
return name;
}
int length = name.length();
if (length == 1) {
return name;
} else if (length == 2) {
return name.substring(0, 1) + "*";
} else {
return name.substring(0, 1) + "*" + name.substring(length - 1);
}
}
/**
* 银行卡脱敏(6226****1234)
*/
public static String maskBankCard(String bankCard) {
if (isEmpty(bankCard) || bankCard.length() < 8) {
return bankCard;
}
return bankCard.replaceAll("(\\d{4})\\d+(\\d{4})", "$1****$2");
}
/**
* 校验手机号
*/
public static boolean isMobile(String mobile) {
if (isEmpty(mobile)) {
return false;
}
return MOBILE_PATTERN.matcher(mobile).matches();
}
/**
* 校验身份证(18位)
*/
public static boolean isIdCard(String idCard) {
if (isEmpty(idCard)) {
return false;
}
return ID_CARD_PATTERN.matcher(idCard).matches();
}
/**
* 空判断(兼容空白字符)
*/
public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}
/**
* 非空判断
*/
public static boolean isNotBlank(String str) {
return !isBlank(str);
}
/**
* 生成随机字符串
*/
public static String randomStr(int length) {
if (length <= 0) {
return "";
}
String chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt((int) (Math.random() * chars.length())));
}
return sb.toString();
}
}
3.2 DateUtil(时间工具)
java
package com.xxx.common.util;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
/**
* 时间工具类(LocalDateTime处理、格式化、时区转换)
* 常用度:100%
* 所属层级:通用层
*
* @author xxx
* @date 2025-11-30
*/
public class DateUtil {
/**
* 默认时间格式
*/
public static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* 日期格式(无时间)
*/
public static final String DATE_PATTERN = "yyyy-MM-dd";
/**
* 时区(默认东八区)
*/
private static final ZoneId DEFAULT_ZONE_ID = ZoneId.of("Asia/Shanghai");
/**
* LocalDateTime转String
*/
public static String format(LocalDateTime dateTime) {
return format(dateTime, DEFAULT_PATTERN);
}
/**
* LocalDateTime转String(自定义格式)
*/
public static String format(LocalDateTime dateTime, String pattern) {
if (dateTime == null) {
return "";
}
return DateTimeFormatter.ofPattern(pattern).format(dateTime);
}
/**
* String转LocalDateTime
*/
public static LocalDateTime parse(String dateStr) {
return parse(dateStr, DEFAULT_PATTERN);
}
/**
* String转LocalDateTime(自定义格式)
*/
public static LocalDateTime parse(String dateStr, String pattern) {
if (StringUtil.isBlank(dateStr)) {
return null;
}
return LocalDateTime.parse(dateStr, DateTimeFormatter.ofPattern(pattern));
}
/**
* Date转LocalDateTime
*/
public static LocalDateTime dateToLocalDateTime(Date date) {
if (date == null) {
return null;
}
return date.toInstant().atZone(DEFAULT_ZONE_ID).toLocalDateTime();
}
/**
* LocalDateTime转Date
*/
public static Date localDateTimeToDate(LocalDateTime dateTime) {
if (dateTime == null) {
return null;
}
return Date.from(dateTime.atZone(DEFAULT_ZONE_ID).toInstant());
}
/**
* 获取当前时间(东八区)
*/
public static LocalDateTime now() {
return LocalDateTime.now(DEFAULT_ZONE_ID);
}
/**
* 计算两个时间的间隔(秒)
*/
public static long betweenSeconds(LocalDateTime start, LocalDateTime end) {
if (start == null || end == null) {
return 0;
}
return ChronoUnit.SECONDS.between(start, end);
}
/**
* 计算相对时间(如:5分钟前、1小时前)
*/
public static String relativeTime(LocalDateTime dateTime) {
if (dateTime == null) {
return "";
}
LocalDateTime now = now();
long seconds = betweenSeconds(dateTime, now);
if (seconds < 60) {
return seconds + "秒前";
} else if (seconds < 3600) {
return seconds / 60 + "分钟前";
} else if (seconds < 86400) {
return seconds / 3600 + "小时前";
} else if (seconds < 2592000) {
return seconds / 86400 + "天前";
} else {
return format(dateTime, DATE_PATTERN);
}
}
/**
* 获取当天开始时间(00:00:00)
*/
public static LocalDateTime getDayStart(LocalDateTime dateTime) {
if (dateTime == null) {
return null;
}
return dateTime.withHour(0).withMinute(0).withSecond(0).withNano(0);
}
/**
* 获取当天结束时间(23:59:59)
*/
public static LocalDateTime getDayEnd(LocalDateTime dateTime) {
if (dateTime == null) {
return null;
}
return dateTime.withHour(23).withMinute(59).withSecond(59).withNano(999999999);
}
}
3.3 EncryptUtil(加密工具)
java
package com.xxx.common.security.util;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.crypto.symmetric.SM4;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* 加密工具类(AES/SM4/MD5,密钥从配置中心读取)
* 常用度:100%
* 所属层级:通用层
*
* @author xxx
* @date 2025-11-30
*/
@Component
public class EncryptUtil {
/**
* AES密钥(生产环境从配置中心/密钥管理系统读取)
*/
@Value("${encrypt.aes.key:xxx1234567890abc}")
private String aesKey;
/**
* SM4密钥(国密算法)
*/
@Value("${encrypt.sm4.key:xxx1234567890abc}")
private String sm4Key;
/**
* MD5加密
*/
public static String md5(String content) {
if (StringUtil.isBlank(content)) {
return "";
}
return SecureUtil.md5(content);
}
/**
* AES加密
*/
public String aesEncrypt(String content) {
if (StringUtil.isBlank(content)) {
return "";
}
AES aes = SecureUtil.aes(aesKey.getBytes(StandardCharsets.UTF_8));
return aes.encryptHex(content);
}
/**
* AES解密
*/
public String aesDecrypt(String encryptStr) {
if (StringUtil.isBlank(encryptStr)) {
return "";
}
AES aes = SecureUtil.aes(aesKey.getBytes(StandardCharsets.UTF_8));
return aes.decryptStr(encryptStr);
}
/**
* SM4加密(国密)
*/
public String sm4Encrypt(String content) {
if (StringUtil.isBlank(content)) {
return "";
}
SM4 sm4 = new SM4(sm4Key.getBytes(StandardCharsets.UTF_8));
return sm4.encryptHex(content);
}
/**
* SM4解密(国密)
*/
public String sm4Decrypt(String encryptStr) {
if (StringUtil.isBlank(encryptStr)) {
return "";
}
SM4 sm4 = new SM4(sm4Key.getBytes(StandardCharsets.UTF_8));
return sm4.decryptStr(encryptStr);
}
/**
* 生成加盐MD5
*/
public static String md5WithSalt(String content, String salt) {
if (StringUtil.isBlank(content) || StringUtil.isBlank(salt)) {
return "";
}
return SecureUtil.md5(content + salt);
}
}
3.4 NumberUtil(数字工具)
java
package com.xxx.common.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 数字工具类(金额处理、精度校验,避免精度丢失)
* 常用度:100%
* 所属层级:通用层(业务/控制)
*
* @author xxx
* @date 2025-11-30
*/
public class NumberUtil {
/**
* 默认小数位数(金额)
*/
private static final int DEFAULT_SCALE = 2;
/**
* 四舍五入
*/
private static final RoundingMode DEFAULT_ROUND_MODE = RoundingMode.HALF_UP;
/**
* 字符串转BigDecimal(空值返回0)
*/
public static BigDecimal toBigDecimal(String str) {
if (StringUtil.isBlank(str)) {
return BigDecimal.ZERO;
}
try {
return new BigDecimal(str);
} catch (NumberFormatException e) {
return BigDecimal.ZERO;
}
}
/**
* 数字转BigDecimal(空值返回0)
*/
public static BigDecimal toBigDecimal(Number num) {
if (num == null) {
return BigDecimal.ZERO;
}
return new BigDecimal(num.toString());
}
/**
* 金额加法(保留2位小数)
*/
public static BigDecimal add(BigDecimal num1, BigDecimal num2) {
return num1.add(num2).setScale(DEFAULT_SCALE, DEFAULT_ROUND_MODE);
}
/**
* 金额减法(保留2位小数)
*/
public static BigDecimal subtract(BigDecimal num1, BigDecimal num2) {
return num1.subtract(num2).setScale(DEFAULT_SCALE, DEFAULT_ROUND_MODE);
}
/**
* 金额乘法(保留2位小数)
*/
public static BigDecimal multiply(BigDecimal num1, BigDecimal num2) {
return num1.multiply(num2).setScale(DEFAULT_SCALE, DEFAULT_ROUND_MODE);
}
/**
* 金额除法(保留2位小数)
*/
public static BigDecimal divide(BigDecimal num1, BigDecimal num2) {
if (num2.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
return num1.divide(num2, DEFAULT_SCALE, DEFAULT_ROUND_MODE);
}
/**
* 元转分(避免浮点数问题)
*/
public static long yuanToFen(BigDecimal yuan) {
return multiply(yuan, new BigDecimal("100")).longValue();
}
/**
* 分转元(保留2位小数)
*/
public static BigDecimal fenToYuan(long fen) {
return divide(new BigDecimal(fen), new BigDecimal("100"));
}
/**
* 判断是否为正数
*/
public static boolean isPositive(BigDecimal num) {
return num.compareTo(BigDecimal.ZERO) > 0;
}
/**
* 判断是否为非负数
*/
public static boolean isNonNegative(BigDecimal num) {
return num.compareTo(BigDecimal.ZERO) >= 0;
}
/**
* 数字格式化(保留指定小数位)
*/
public static String format(BigDecimal num, int scale) {
return num.setScale(scale, DEFAULT_ROUND_MODE).toString();
}
}
3.5 CollectionUtil(集合工具)
java
package com.xxx.common.util;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 集合工具类(空判断、列表转树、去重)
* 常用度:100%
* 所属层级:通用层
*
* @author xxx
* @date 2025-11-30
*/
public class CollectionUtil extends CollectionUtils {
/**
* 空判断
*/
public static boolean isEmpty(Collection<?> coll) {
return coll == null || coll.isEmpty();
}
/**
* 非空判断
*/
public static boolean isNotEmpty(Collection<?> coll) {
return !isEmpty(coll);
}
/**
* Map空判断
*/
public static boolean isEmpty(Map<?, ?> map) {
return MapUtils.isEmpty(map);
}
/**
* Map非空判断
*/
public static boolean isNotEmpty(Map<?, ?> map) {
return MapUtils.isNotEmpty(map);
}
/**
* 列表去重(根据指定字段)
*/
public static <T, K> List<T> distinctByField(List<T> list, Function<T, K> keyExtractor) {
if (isEmpty(list)) {
return new ArrayList<>();
}
Map<K, T> map = new LinkedHashMap<>();
for (T t : list) {
K key = keyExtractor.apply(t);
if (key != null && !map.containsKey(key)) {
map.put(key, t);
}
}
return new ArrayList<>(map.values());
}
/**
* 列表转树结构(通用)
*
* @param list 源列表
* @param idGetter ID获取器
* @param parentIdGetter 父ID获取器
* @param childrenSetter 子节点设置器
* @param <T> 节点类型
* @param <ID> ID类型
* @return 树结构列表
*/
public static <T, ID> List<T> listToTree(List<T> list,
Function<T, ID> idGetter,
Function<T, ID> parentIdGetter,
java.util.function.BiConsumer<T, List<T>> childrenSetter) {
if (isEmpty(list)) {
return new ArrayList<>();
}
// 构建ID->节点映射
Map<ID, T> idNodeMap = list.stream()
.collect(Collectors.toMap(idGetter, t -> t, (t1, t2) -> t1));
// 构建树
List<T> rootList = new ArrayList<>();
for (T node : list) {
ID parentId = parentIdGetter.apply(node);
if (parentId == null || !idNodeMap.containsKey(parentId)) {
// 根节点
rootList.add(node);
} else {
// 子节点
T parentNode = idNodeMap.get(parentId);
// 获取父节点的子列表
List<T> children = new ArrayList<>();
// 反射/自定义方式获取子列表(此处简化,实际可通过BeanUtil获取)
childrenSetter.accept(parentNode, children);
children.add(node);
}
}
return rootList;
}
/**
* 列表转Map(指定Key,值为单个对象)
*/
public static <T, K> Map<K, T> listToMap(List<T> list, Function<T, K> keyExtractor) {
if (isEmpty(list)) {
return new HashMap<>();
}
return list.stream()
.collect(Collectors.toMap(keyExtractor, t -> t, (t1, t2) -> t1));
}
/**
* 列表转Map(指定Key,值为列表)
*/
public static <T, K> Map<K, List<T>> listToMapList(List<T> list, Function<T, K> keyExtractor) {
if (isEmpty(list)) {
return new HashMap<>();
}
return list.stream()
.collect(Collectors.groupingBy(keyExtractor));
}
}
3.6 BeanUtil(对象转换)
java
package com.xxx.common.util;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import java.beans.PropertyDescriptor;
import java.util.HashSet;
import java.util.Set;
/**
* 对象转换工具类(深拷贝、Bean转Map,基于Spring/CommonsBeanutils)
* 常用度:100%
* 所属层级:通用层
*
* @author xxx
* @date 2025-11-30
*/
public class BeanUtil extends org.apache.commons.beanutils.BeanUtils {
/**
* 浅拷贝(忽略空值)
*/
public static void copyPropertiesIgnoreNull(Object source, Object target) {
BeanUtils.copyProperties(source, target, getNullPropertyNames(source));
}
/**
* 深拷贝(创建新对象)
*/
public static <T> T deepCopy(Object source, Class<T> targetClass) {
if (source == null) {
return null;
}
try {
T target = targetClass.getDeclaredConstructor().newInstance();
copyPropertiesIgnoreNull(source, target);
return target;
} catch (Exception e) {
throw new RuntimeException("Bean深拷贝失败", e);
}
}
/**
* 获取空值属性名
*/
private static String[] getNullPropertyNames(Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
PropertyDescriptor[] pds = src.getPropertyDescriptors();
Set<String> emptyNames = new HashSet<>();
for (PropertyDescriptor pd : pds) {
Object srcValue = src.getPropertyValue(pd.getName());
if (srcValue == null) {
emptyNames.add(pd.getName());
}
}
return emptyNames.toArray(new String[0]);
}
/**
* Bean转Map(忽略空值)
*/
public static java.util.Map<String, Object> beanToMap(Object bean) {
if (bean == null) {
return new java.util.HashMap<>();
}
java.util.Map<String, Object> map = new java.util.HashMap<>();
try {
PropertyDescriptor[] pds = new BeanWrapperImpl(bean).getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
String name = pd.getName();
if (!"class".equals(name)) {
Object value = pd.getReadMethod().invoke(bean);
if (value != null) {
map.put(name, value);
}
}
}
return map;
} catch (Exception e) {
throw new RuntimeException("Bean转Map失败", e);
}
}
/**
* Map转Bean
*/
public static <T> T mapToBean(java.util.Map<String, Object> map, Class<T> beanClass) {
if (CollectionUtil.isEmpty(map)) {
return null;
}
try {
T bean = beanClass.getDeclaredConstructor().newInstance();
org.apache.commons.beanutils.BeanUtils.populate(bean, map);
return bean;
} catch (Exception e) {
throw new RuntimeException("Map转Bean失败", e);
}
}
}
二、接口交互模块(接口开发必用,100%通用)
1. 统一返回相关
1.1 ApiResponse(全局返回体)
java
package com.xxx.common.web.response;
import com.xxx.common.enums.ApiResultCode;
import com.xxx.common.enums.HttpStatusEnum;
import lombok.Data;
import java.io.Serializable;
/**
* 全局统一返回体(含code/msg/data/httpCode/traceId)
* 常用度:100%
* 所属层级:控制层
*
* @author xxx
* @date 2025-11-30
*/
@Data
public class ApiResponse<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 业务返回码
*/
private int code;
/**
* 返回消息
*/
private String msg;
/**
* 返回数据
*/
private T data;
/**
* HTTP状态码
*/
private int httpCode;
/**
* 全链路TraceId
*/
private String traceId;
/**
* 成功返回(无数据)
*/
public static <T> ApiResponse<T> success() {
return success(null);
}
/**
* 成功返回(带数据)
*/
public static <T> ApiResponse<T> success(T data) {
ApiResultCode successCode = ApiResultCode.SUCCESS;
ApiResponse<T> response = new ApiResponse<>();
response.setCode(successCode.getCode());
response.setMsg(successCode.getMsg());
response.setData(data);
response.setHttpCode(HttpStatusEnum.SUCCESS.getCode());
response.setTraceId(TraceIdUtil.getTraceId()); // 后续实现TraceIdUtil
return response;
}
/**
* 失败返回(自定义业务码)
*/
public static <T> ApiResponse<T> fail(ApiResultCode resultCode) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(resultCode.getCode());
response.setMsg(resultCode.getMsg());
response.setHttpCode(HttpStatusEnum.matchByBizCode(resultCode.getBizCode()).getCode());
response.setTraceId(TraceIdUtil.getTraceId());
return response;
}
/**
* 失败返回(自定义消息)
*/
public static <T> ApiResponse<T> fail(int code, String msg) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(code);
response.setMsg(msg);
response.setHttpCode(HttpStatusEnum.BAD_REQUEST.getCode());
response.setTraceId(TraceIdUtil.getTraceId());
return response;
}
}
1.2 PageParam(分页请求参数)
java
package com.xxx.common.web.param;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.Data;
import java.io.Serializable;
/**
* 分页请求参数(页码/页大小校验,支持排序)
* 常用度:100%
* 所属层级:控制层
*
* @author xxx
* @date 2025-11-30
*/
@Data
public class PageParam implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 当前页码(默认1)
*/
@Min(value = 1, message = "页码不能小于1")
private Integer pageNum = 1;
/**
* 页大小(默认10,最大100)
*/
@Min(value = 1, message = "页大小不能小于1")
@Max(value = 100, message = "页大小不能超过100")
private Integer pageSize = 10;
/**
* 排序字段(如:createTime)
*/
private String sortField;
/**
* 排序方式(asc/desc,默认desc)
*/
private String sortType = "desc";
/**
* 获取MyBatisPlus分页对象
*/
public com.baomidou.mybatisplus.extension.plugins.pagination.Page<?> toPage() {
return new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageNum, pageSize);
}
}
1.3 PageResult(分页结果)
java
package com.xxx.common.web.response;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 分页结果(兼容MyBatisPlus IPage)
* 常用度:100%
* 所属层级:控制层
*
* @author xxx
* @date 2025-11-30
*/
@Data
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 当前页码
*/
private Integer pageNum;
/**
* 页大小
*/
private Integer pageSize;
/**
* 总条数
*/
private Long total;
/**
* 总页数
*/
private Integer pages;
/**
* 数据列表
*/
private List<T> list;
/**
* 从MyBatisPlus Page转换
*/
public static <T> PageResult<T> from(Page<T> page) {
PageResult<T> result = new PageResult<>();
result.setPageNum((int) page.getCurrent());
result.setPageSize((int) page.getSize());
result.setTotal(page.getTotal());
result.setPages((int) page.getPages());
result.setList(page.getRecords());
return result;
}
/**
* 自定义分页结果
*/
public static <T> PageResult<T> of(Integer pageNum, Integer pageSize, Long total, List<T> list) {
PageResult<T> result = new PageResult<>();
result.setPageNum(pageNum);
result.setPageSize(pageSize);
result.setTotal(total);
result.setPages(total % pageSize == 0 ? (int) (total / pageSize) : (int) (total / pageSize) + 1);
result.setList(list);
return result;
}
}
1.4 ApiResultCode(返回码枚举)
java
package com.xxx.common.enums;
import lombok.Getter;
/**
* 接口返回码枚举(与HTTP状态码绑定)
* 常用度:100%
* 所属层级:通用层(控制/业务)
*
* @author xxx
* @date 2025-11-30
*/
@Getter
public enum ApiResultCode {
/**
* 成功
*/
SUCCESS(20000, "操作成功", ErrorCodeEnum.SYSTEM_ERROR),
/**
* 失败
*/
FAIL(40000, "操作失败", ErrorCodeEnum.BUSINESS_COMMON_ERROR),
/**
* 参数无效
*/
PARAM_INVALID(40001, "参数无效", ErrorCodeEnum.PARAM_INVALID),
/**
* 未认证
*/
UNAUTHORIZED(40100, "未认证", ErrorCodeEnum.TOKEN_INVALID),
/**
* 权限不足
*/
FORBIDDEN(40300, "权限不足", ErrorCodeEnum.PERMISSION_DENIED),
/**
* 资源不存在
*/
NOT_FOUND(40400, "资源不存在", ErrorCodeEnum.DATA_NOT_FOUND),
/**
* 系统异常
*/
SYSTEM_ERROR(50000, "系统异常", ErrorCodeEnum.SYSTEM_ERROR),
/**
* 请求过于频繁
*/
TOO_MANY_REQUESTS(42900, "请求过于频繁", ErrorCodeEnum.SYSTEM_ERROR);
/**
* 返回码
*/
private final int code;
/**
* 返回消息
*/
private final String msg;
/**
* 关联业务错误码
*/
private final ErrorCodeEnum bizCode;
ApiResultCode(int code, String msg, ErrorCodeEnum bizCode) {
this.code = code;
this.msg = msg;
this.bizCode = bizCode;
}
}
2. 参数校验相关
2.1 GroupValidator(分组校验接口)
java
package com.xxx.common.web.validator.group;
/**
* 参数校验分组(区分新增/更新场景)
* 常用度:100%
* 所属层级:控制层
*
* @author xxx
* @date 2025-11-30
*/
public interface GroupValidator {
/**
* 新增分组
*/
interface AddGroup {}
/**
* 更新分组
*/
interface UpdateGroup {}
/**
* 删除分组
*/
interface DeleteGroup {}
/**
* 查询分组
*/
interface QueryGroup {}
}
2.2 CustomValidAnnotations(自定义校验注解)
java
package com.xxx.common.web.validator.annotation;
import com.xxx.common.web.validator.ConstraintValidators.MobileValidator;
import com.xxx.common.web.validator.ConstraintValidators.IdCardValidator;
import com.xxx.common.web.validator.ConstraintValidators.NotBlankOrNullValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
/**
* 自定义校验注解(@Mobile/@IdCard/@NotBlankOrNull)
* 常用度:100%
* 所属层级:控制层
*
* @author xxx
* @date 2025-11-30
*/
public class CustomValidAnnotations {
/**
* 手机号校验注解
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
/**
* 身份证校验注解
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = IdCardValidator.class)
public @interface IdCard {
String message() default "身份证格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
/**
* 非空或null校验(允许空字符串,但不允许null)
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = NotBlankOrNullValidator.class)
public @interface NotBlankOrNull {
String message() default "字段不能为空字符串";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
}
// 校验器实现类(单独存放)
package com.xxx.common.web.validator.ConstraintValidators;
import com.xxx.common.util.StringUtil;
import com.xxx.common.web.validator.annotation.CustomValidAnnotations;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
/**
* 手机号校验器
*/
public class MobileValidator implements ConstraintValidator<CustomValidAnnotations.Mobile, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 允许null(如需非空,配合@NotNull使用)
if (StringUtil.isBlank(value)) {
return true;
}
return StringUtil.isMobile(value);
}
}
/**
* 身份证校验器
*/
public class IdCardValidator implements ConstraintValidator<CustomValidAnnotations.IdCard, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtil.isBlank(value)) {
return true;
}
return StringUtil.isIdCard(value);
}
}
/**
* 非空或null校验器
*/
public class NotBlankOrNullValidator implements ConstraintValidator<CustomValidAnnotations.NotBlankOrNull, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 允许空字符串,但不允许null
return value != null;
}
}
2.3 ValidUtil(手动校验工具)
java
package com.xxx.common.web.validator;
import com.xxx.common.exception.ParamInvalidException;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 参数手动校验工具(抛ParamInvalidException)
* 常用度:100%
* 所属层级:控制层/业务层
*
* @author xxx
* @date 2025-11-30
*/
public class ValidUtil {
private static final Validator VALIDATOR;
static {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
VALIDATOR = factory.getValidator();
}
/**
* 手动校验对象(默认分组)
*/
public static <T> void validate(T obj) {
validate(obj, new Class<?>[]{});
}
/**
* 手动校验对象(指定分组)
*/
public static <T> void validate(T obj, Class<?>... groups) {
if (obj == null) {
throw new ParamInvalidException("校验对象不能为空");
}
Set<ConstraintViolation<T>> violations = VALIDATOR.validate(obj, groups);
if (!violations.isEmpty()) {
// 拼接错误信息(如:username不能为空, mobile格式不正确)
String errorMsg = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
throw new ParamInvalidException(errorMsg);
}
}
/**
* 校验单个字段
*/
public static <T> void validateField(T obj, String fieldName) {
Set<ConstraintViolation<T>> violations = VALIDATOR.validateProperty(obj, fieldName);
if (!violations.isEmpty()) {
String errorMsg = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
throw new ParamInvalidException(fieldName + ":" + errorMsg);
}
}
}
三、业务逻辑模块(业务开发必用,95%通用)
1. 异常处理
1.1 GlobalExceptionHandler(全局异常拦截)
java
package com.xxx.common.web.exception;
import com.xxx.common.enums.ApiResultCode;
import com.xxx.common.enums.EnvEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.exception.ParamInvalidException;
import com.xxx.common.exception.SystemException;
import com.xxx.common.monitor.util.ExceptionLogUtil;
import com.xxx.common.util.StringUtil;
import com.xxx.common.web.response.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.converter.HttpMessageNotReadableException;
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.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.List;
import java.util.stream.Collectors;
/**
* 全局异常处理器(区分业务/系统/参数异常,生产环境隐藏堆栈)
* 常用度:100%
* 所属层级:控制层
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 环境标识(从配置文件读取)
*/
@Value("${spring.profiles.active:dev}")
private String env;
/**
* 业务异常处理
*/
@ExceptionHandler(BusinessException.class)
public ApiResponse<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
// 记录业务异常日志(级别为WARN)
log.warn("[业务异常] URI: {}, 错误码: {}, 消息: {}",
request.getRequestURI(), e.getErrorCode().getCode(), e.getMessage());
// 记录异常审计日志
ExceptionLogUtil.recordBusinessException(e, request);
return ApiResponse.fail(ApiResultCode.FAIL.getCode(), e.getMessage());
}
/**
* 参数异常处理
*/
@ExceptionHandler(ParamInvalidException.class)
public ApiResponse<Void> handleParamInvalidException(ParamInvalidException e, HttpServletRequest request) {
log.warn("[参数异常] URI: {}, 错误信息: {}", request.getRequestURI(), e.getMessage());
ExceptionLogUtil.recordParamException(e, request);
return ApiResponse.fail(ApiResultCode.PARAM_INVALID.getCode(), e.getMessage());
}
/**
* 系统异常处理
*/
@ExceptionHandler(SystemException.class)
public ApiResponse<Void> handleSystemException(SystemException e, HttpServletRequest request) {
// 系统异常记录ERROR级别日志
log.error("[系统异常] URI: {}", request.getRequestURI(), e);
ExceptionLogUtil.recordSystemException(e, request);
// 生产环境隐藏具体异常信息
String msg = EnvEnum.getByCode(env).isProd() ? ApiResultCode.SYSTEM_ERROR.getMsg() : e.getMessage();
return ApiResponse.fail(ApiResultCode.SYSTEM_ERROR.getCode(), msg);
}
/**
* 接口参数校验异常(@Valid)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
String errorMsg = fieldErrors.stream()
.map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
.collect(Collectors.joining("; "));
log.warn("[参数校验异常] URI: {}, 错误信息: {}", request.getRequestURI(), errorMsg);
return ApiResponse.fail(ApiResultCode.PARAM_INVALID.getCode(), errorMsg);
}
/**
* 请求参数绑定异常
*/
@ExceptionHandler(BindException.class)
public ApiResponse<Void> handleBindException(BindException e, HttpServletRequest request) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
String errorMsg = fieldErrors.stream()
.map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
.collect(Collectors.joining("; "));
log.warn("[参数绑定异常] URI: {}, 错误信息: {}", request.getRequestURI(), errorMsg);
return ApiResponse.fail(ApiResultCode.PARAM_INVALID.getCode(), errorMsg);
}
/**
* 请求体解析异常(如JSON格式错误)
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ApiResponse<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
log.warn("[请求体解析异常] URI: {}, 错误信息: {}", request.getRequestURI(), e.getMessage());
String msg = "请求体格式错误,请检查JSON格式";
return ApiResponse.fail(ApiResultCode.PARAM_INVALID.getCode(), msg);
}
/**
* 404异常
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ApiResponse<Void> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
log.warn("[404异常] URI: {}, 错误信息: {}", request.getRequestURI(), e.getMessage());
return ApiResponse.fail(ApiResultCode.NOT_FOUND.getCode(), "接口不存在");
}
/**
* 兜底异常处理
*/
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e, HttpServletRequest request) {
log.error("[未知异常] URI: {}", request.getRequestURI(), e);
ExceptionLogUtil.recordUnknownException(e, request);
String msg = EnvEnum.getByCode(env).isProd() ? ApiResultCode.SYSTEM_ERROR.getMsg() : e.getMessage();
return ApiResponse.fail(ApiResultCode.SYSTEM_ERROR.getCode(), msg);
}
}
1.2 BusinessException(业务异常)
java
package com.xxx.common.exception;
import com.xxx.common.enums.ErrorCodeEnum;
import lombok.Getter;
/**
* 业务异常(携带错误码,支持自定义消息)
* 常用度:100%
* 所属层级:业务层/控制层
*
* @author xxx
* @date 2025-11-30
*/
@Getter
public class BusinessException extends RuntimeException {
/**
* 业务错误码
*/
private final ErrorCodeEnum errorCode;
/**
* 构造器(默认错误码)
*/
public BusinessException(String message) {
super(message);
this.errorCode = ErrorCodeEnum.BUSINESS_COMMON_ERROR;
}
/**
* 构造器(自定义错误码)
*/
public BusinessException(ErrorCodeEnum errorCode) {
super(errorCode.getMsg());
this.errorCode = errorCode;
}
/**
* 构造器(自定义错误码+消息)
*/
public BusinessException(ErrorCodeEnum errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
/**
* 构造器(自定义消息+异常)
*/
public BusinessException(String message, Throwable cause) {
super(message, cause);
this.errorCode = ErrorCodeEnum.BUSINESS_COMMON_ERROR;
}
}
1.3 SystemException(系统异常)
java
package com.xxx.common.exception;
import com.xxx.common.enums.ErrorCodeEnum;
import lombok.Getter;
/**
* 系统异常(携带原始异常,便于排查)
* 常用度:100%
* 所属层级:通用层(业务/DAO/控制)
*
* @author xxx
* @date 2025-11-30
*/
@Getter
public class SystemException extends RuntimeException {
/**
* 系统错误码
*/
private final ErrorCodeEnum errorCode;
/**
* 构造器(默认错误码)
*/
public SystemException(String message) {
super(message);
this.errorCode = ErrorCodeEnum.SYSTEM_ERROR;
}
/**
* 构造器(自定义错误码)
*/
public SystemException(ErrorCodeEnum errorCode) {
super(errorCode.getMsg());
this.errorCode = errorCode;
}
/**
* 构造器(原始异常+默认错误码)
*/
public SystemException(Throwable cause) {
super(cause);
this.errorCode = ErrorCodeEnum.SYSTEM_ERROR;
}
/**
* 构造器(自定义消息+原始异常)
*/
public SystemException(String message, Throwable cause) {
super(message, cause);
this.errorCode = ErrorCodeEnum.SYSTEM_ERROR;
}
/**
* 构造器(自定义错误码+原始异常)
*/
public SystemException(ErrorCodeEnum errorCode, Throwable cause) {
super(errorCode.getMsg(), cause);
this.errorCode = errorCode;
}
}
1.4 ParamInvalidException(参数异常)
java
package com.xxx.common.exception;
import com.xxx.common.enums.ErrorCodeEnum;
import lombok.Getter;
import java.util.List;
/**
* 参数异常(返回错误字段列表)
* 常用度:100%
* 所属层级:控制层/业务层
*
* @author xxx
* @date 2025-11-30
*/
@Getter
public class ParamInvalidException extends RuntimeException {
/**
* 错误字段列表
*/
private final List<String> errorFields;
/**
* 构造器(单条错误信息)
*/
public ParamInvalidException(String message) {
super(message);
this.errorFields = List.of(message);
}
/**
* 构造器(错误字段列表)
*/
public ParamInvalidException(List<String> errorFields) {
super(String.join("; ", errorFields));
this.errorFields = errorFields;
}
/**
* 构造器(字段名+错误信息)
*/
public ParamInvalidException(String fieldName, String message) {
super(String.format("%s: %s", fieldName, message));
this.errorFields = List.of(super.getMessage());
}
/**
* 获取错误码
*/
public ErrorCodeEnum getErrorCode() {
return ErrorCodeEnum.PARAM_INVALID;
}
}
2. 业务工具类
2.1 ExcelUtil(EasyExcel导入导出)
java
package com.xxx.common.business.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.enums.ErrorCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* Excel工具类(EasyExcel封装,大文件分片、数据校验)
* 常用度:95%
* 所属层级:业务层/控制层
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
public class ExcelUtil {
/**
* Excel导出(单sheet)
*/
public static <T> void export(HttpServletResponse response,
String fileName,
String sheetName,
Class<T> clazz,
List<T> data) {
try {
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 防止中文文件名乱码
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");
// 写入Excel
EasyExcel.write(response.getOutputStream(), clazz)
.excelType(ExcelTypeEnum.XLSX)
.sheet(sheetName)
.doWrite(data);
} catch (IOException e) {
log.error("Excel导出失败", e);
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "Excel导出失败:" + e.getMessage());
}
}
/**
* Excel导入(小文件,一次性读取)
*/
public static <T> List<T> importExcel(MultipartFile file, Class<T> clazz) {
if (file == null || file.isEmpty()) {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "导入文件不能为空");
}
// 校验文件类型
String originalFilename = file.getOriginalFilename();
if (StringUtils.isBlank(originalFilename) ||
(!originalFilename.endsWith(".xlsx") && !originalFilename.endsWith(".xls"))) {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "仅支持.xlsx/.xls格式文件");
}
List<T> dataList = new ArrayList<>();
try {
EasyExcel.read(file.getInputStream(), clazz, new AnalysisEventListener<T>() {
@Override
public void invoke(T data, AnalysisContext context) {
dataList.add(data);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("Excel导入完成,共读取{}条数据", dataList.size());
}
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
log.error("Excel解析异常,行号:{}", context.readRowHolder().getRowIndex() + 1, exception);
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR,
String.format("Excel解析失败,行号:%d,原因:%s",
context.readRowHolder().getRowIndex() + 1, exception.getMessage()));
}
}).excelType(ExcelTypeEnum.valueOf(originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toUpperCase()))
.sheet()
.doRead();
return dataList;
} catch (IOException e) {
log.error("Excel导入失败", e);
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "Excel导入失败:" + e.getMessage());
}
}
/**
* Excel导入(大文件,分片处理)
*/
public static <T> void importLargeExcel(MultipartFile file,
Class<T> clazz,
int batchSize,
Consumer<List<T>> batchConsumer) {
if (file == null || file.isEmpty()) {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "导入文件不能为空");
}
String originalFilename = file.getOriginalFilename();
if (StringUtils.isBlank(originalFilename) ||
(!originalFilename.endsWith(".xlsx") && !originalFilename.endsWith(".xls"))) {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "仅支持.xlsx/.xls格式文件");
}
try {
EasyExcel.read(file.getInputStream(), clazz, new AnalysisEventListener<T>() {
private List<T> batchData = new ArrayList<>(batchSize);
@Override
public void invoke(T data, AnalysisContext context) {
batchData.add(data);
// 达到批次大小,处理数据
if (batchData.size() >= batchSize) {
batchConsumer.accept(batchData);
batchData.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 处理剩余数据
if (!batchData.isEmpty()) {
batchConsumer.accept(batchData);
}
log.info("大文件Excel导入完成,总行数:{}", context.readRowHolder().getRowIndex());
}
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
log.error("大文件Excel解析异常,行号:{}", context.readRowHolder().getRowIndex() + 1, exception);
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR,
String.format("Excel解析失败,行号:%d,原因:%s",
context.readRowHolder().getRowIndex() + 1, exception.getMessage()));
}
}).excelType(ExcelTypeEnum.valueOf(originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toUpperCase()))
.sheet()
.doRead();
} catch (IOException e) {
log.error("大文件Excel导入失败", e);
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "大文件Excel导入失败:" + e.getMessage());
}
}
}
2.2 IdGeneratorUtil(分布式ID生成)
java
package com.xxx.common.business.util;
import com.xxx.common.exception.SystemException;
import com.xxx.common.enums.ErrorCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.concurrent.atomic.AtomicLong;
/**
* 分布式ID生成工具(雪花算法,兼容数据库自增ID)
* 常用度:95%
* 所属层级:通用层(业务/DAO)
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Component
public class IdGeneratorUtil {
/**
* 雪花算法参数
*/
private static final long TWEPOCH = 1609459200000L; // 2021-01-01 00:00:00
private static final long WORKER_ID_BITS = 5L;
private static final long DATACENTER_ID_BITS = 5L;
private static final long MAX_WORKER_ID = (1L << WORKER_ID_BITS) - 1;
private static final long MAX_DATACENTER_ID = (1L << DATACENTER_ID_BITS) - 1;
private static final long SEQUENCE_BITS = 12L;
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
private static final long SEQUENCE_MASK = (1L << SEQUENCE_BITS) - 1;
/**
* 工作节点ID(从配置文件读取,默认自动生成)
*/
@Value("${id-generator.worker-id:-1}")
private long workerId;
/**
* 数据中心ID(从配置文件读取,默认自动生成)
*/
@Value("${id-generator.datacenter-id:-1}")
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
private final AtomicLong atomicSequence = new AtomicLong(0);
/**
* 初始化工作节点ID和数据中心ID
*/
@PostConstruct
public void init() {
if (workerId == -1) {
workerId = generateWorkerId();
}
if (datacenterId == -1) {
datacenterId = generateDatacenterId();
}
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new SystemException(ErrorCodeEnum.SYSTEM_ERROR,
String.format("Worker ID不能大于%d或小于0", MAX_WORKER_ID));
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new SystemException(ErrorCodeEnum.SYSTEM_ERROR,
String.format("Datacenter ID不能大于%d或小于0", MAX_DATACENTER_ID));
}
log.info("雪花算法初始化完成,workerId={}, datacenterId={}", workerId, datacenterId);
}
/**
* 生成雪花算法ID
*/
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨处理
if (timestamp < lastTimestamp) {
log.error("时钟回拨,lastTimestamp={}, currentTimestamp={}", lastTimestamp, timestamp);
// 等待时钟追上,最多等待3秒
long waitTime = lastTimestamp - timestamp;
if (waitTime > 3000) {
throw new SystemException(ErrorCodeEnum.SYSTEM_ERROR, "时钟回拨超过3秒,无法生成ID");
}
try {
Thread.sleep(waitTime);
timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new SystemException(ErrorCodeEnum.SYSTEM_ERROR, "时钟回拨,生成ID失败");
}
} catch (InterruptedException e) {
throw new SystemException(e);
}
}
// 同一毫秒内,序列号递增
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
// 序列号溢出,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// 组装ID
return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
/**
* 生成数据库自增风格ID(纯数字,递增)
*/
public long nextIncrementId() {
return System.currentTimeMillis() * 10000 + atomicSequence.incrementAndGet() % 10000;
}
/**
* 生成UUID(无横线)
*/
public String nextUuid() {
return java.util.UUID.randomUUID().toString().replace("-", "");
}
/**
* 等待下一毫秒
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
/**
* 自动生成工作节点ID(基于MAC地址+进程ID)
*/
private long generateWorkerId() {
try {
// 获取MAC地址
InetAddress address = InetAddress.getLocalHost();
NetworkInterface ni = NetworkInterface.getByInetAddress(address);
byte[] mac = ni.getHardwareAddress();
long macHash = 0;
for (byte b : mac) {
macHash = (macHash << 8) | (b & 0xff);
}
// 获取进程ID
String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
long pidHash = Long.parseLong(pid);
// 计算workerId
return (macHash ^ pidHash) % MAX_WORKER_ID;
} catch (Exception e) {
log.warn("自动生成workerId失败,使用随机数", e);
return (long) (Math.random() * MAX_WORKER_ID);
}
}
/**
* 自动生成数据中心ID(基于IP地址)
*/
private long generateDatacenterId() {
try {
InetAddress address = InetAddress.getLocalHost();
byte[] ip = address.getAddress();
long ipHash = 0;
for (byte b : ip) {
ipHash = (ipHash << 8) | (b & 0xff);
}
return ipHash % MAX_DATACENTER_ID;
} catch (Exception e) {
log.warn("自动生成datacenterId失败,使用随机数", e);
return (long) (Math.random() * MAX_DATACENTER_ID);
}
}
}
2.3 FileUtil(文件操作工具)
java
package com.xxx.common.business.util;
import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
/**
* 文件操作工具类(兼容MinIO/本地存储,自动生成文件名)
* 常用度:95%
* 所属层级:业务层/控制层
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Component
public class FileUtil {
/**
* 存储类型(local/minio)
*/
@Value("${file.storage.type:local}")
private String storageType;
/**
* 本地存储根路径
*/
@Value("${file.storage.local.root-path:/data/files}")
private String localRootPath;
/**
* MinIO相关配置(示例)
*/
@Value("${file.storage.minio.endpoint:}")
private String minioEndpoint;
@Value("${file.storage.minio.bucket-name:}")
private String minioBucketName;
@Value("${file.storage.minio.access-key:}")
private String minioAccessKey;
@Value("${file.storage.minio.secret-key:}")
private String minioSecretKey;
/**
* 文件上传
*/
public String upload(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "上传文件不能为空");
}
// 校验文件大小(默认最大50MB)
long maxSize = 50 * 1024 * 1024;
if (file.getSize() > maxSize) {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "文件大小不能超过50MB");
}
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String ext = StringUtil.isBlank(originalFilename) ? "" :
originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString() + ext;
// 按日期分目录
String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
try {
if ("local".equals(storageType)) {
// 本地存储
Path dirPath = Paths.get(localRootPath, dateDir);
if (!Files.exists(dirPath)) {
Files.createDirectories(dirPath);
}
Path filePath = dirPath.resolve(fileName);
file.transferTo(filePath);
log.info("本地文件上传成功,路径:{}", filePath);
return String.format("/%s/%s/%s", storageType, dateDir, fileName);
} else if ("minio".equals(storageType)) {
// MinIO存储(需引入minio依赖)
/*
MinioClient minioClient = MinioClient.builder()
.endpoint(minioEndpoint)
.credentials(minioAccessKey, minioSecretKey)
.build();
// 检查桶是否存在
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(minioBucketName).build())) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(minioBucketName).build());
}
// 上传文件
String objectName = String.format("%s/%s", dateDir, fileName);
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minioBucketName)
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
log.info("MinIO文件上传成功,对象名:{}", objectName);
return String.format("%s/%s/%s", minioEndpoint, minioBucketName, objectName);
*/
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "MinIO存储未实现");
} else {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "不支持的存储类型:" + storageType);
}
} catch (IOException e) {
log.error("文件上传失败", e);
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "文件上传失败:" + e.getMessage());
}
}
/**
* 文件下载
*/
public byte[] download(String filePath) {
if (StringUtil.isBlank(filePath)) {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "文件路径不能为空");
}
try {
if (filePath.startsWith("/local/")) {
// 本地文件下载
String realPath = localRootPath + filePath.replace("/local/", "/");
Path path = Paths.get(realPath);
if (!Files.exists(path)) {
throw new BusinessException(ErrorCodeEnum.DATA_NOT_FOUND, "文件不存在");
}
return Files.readAllBytes(path);
} else if (filePath.startsWith(minioEndpoint)) {
// MinIO文件下载
/*
String objectName = filePath.replace(minioEndpoint + "/" + minioBucketName + "/", "");
MinioClient minioClient = MinioClient.builder()
.endpoint(minioEndpoint)
.credentials(minioAccessKey, minioSecretKey)
.build();
InputStream inputStream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(minioBucketName)
.object(objectName)
.build()
);
return IOUtils.toByteArray(inputStream);
*/
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "MinIO下载未实现");
} else {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "不支持的文件路径:" + filePath);
}
} catch (IOException e) {
log.error("文件下载失败", e);
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "文件下载失败:" + e.getMessage());
}
}
/**
* 文件删除
*/
public void delete(String filePath) {
if (StringUtil.isBlank(filePath)) {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "文件路径不能为空");
}
try {
if (filePath.startsWith("/local/")) {
// 本地文件删除
String realPath = localRootPath + filePath.replace("/local/", "/");
Path path = Paths.get(realPath);
if (Files.exists(path)) {
Files.delete(path);
log.info("本地文件删除成功,路径:{}", realPath);
}
} else if (filePath.startsWith(minioEndpoint)) {
// MinIO文件删除
/*
String objectName = filePath.replace(minioEndpoint + "/" + minioBucketName + "/", "");
MinioClient minioClient = MinioClient.builder()
.endpoint(minioEndpoint)
.credentials(minioAccessKey, minioSecretKey)
.build();
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(minioBucketName)
.object(objectName)
.build()
);
log.info("MinIO文件删除成功,对象名:{}", objectName);
*/
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "MinIO删除未实现");
} else {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "不支持的文件路径:" + filePath);
}
} catch (IOException e) {
log.error("文件删除失败", e);
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "文件删除失败:" + e.getMessage());
}
}
/**
* 获取文件扩展名
*/
public static String getFileExt(String fileName) {
if (StringUtil.isBlank(fileName) || !fileName.contains(".")) {
return "";
}
return fileName.substring(fileName.lastIndexOf("."));
}
/**
* 校验文件类型(白名单)
*/
public static boolean checkFileType(String fileName, String[] allowExts) {
String ext = getFileExt(fileName).toLowerCase();
for (String allowExt : allowExts) {
if (ext.equals(allowExt.toLowerCase())) {
return true;
}
}
return false;
}
}
2.4 MoneyUtil(金额计算工具)
java
package com.xxx.common.business.util;
import com.xxx.common.util.NumberUtil;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 金额计算工具类(BigDecimal封装,元转分/分转元)
* 常用度:95%
* 所属层级:业务层
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
public class MoneyUtil {
/**
* 金额小数位数
*/
private static final int MONEY_SCALE = 2;
/**
* 四舍五入模式
*/
private static final RoundingMode ROUND_MODE = RoundingMode.HALF_UP;
/**
* 元转分(精确计算,避免浮点数误差)
*/
public static long yuanToFen(BigDecimal yuan) {
if (yuan == null) {
return 0L;
}
// 先保留2位小数,再转分
BigDecimal fen = yuan.setScale(MONEY_SCALE, ROUND_MODE).multiply(new BigDecimal("100"));
return fen.longValue();
}
/**
* 分转元(保留2位小数)
*/
public static BigDecimal fenToYuan(long fen) {
BigDecimal yuan = new BigDecimal(fen).divide(new BigDecimal("100"), MONEY_SCALE, ROUND_MODE);
return yuan;
}
/**
* 金额加法(元)
*/
public static BigDecimal add(BigDecimal m1, BigDecimal m2) {
return NumberUtil.add(m1, m2);
}
/**
* 金额减法(元)
*/
public static BigDecimal subtract(BigDecimal m1, BigDecimal m2) {
return NumberUtil.subtract(m1, m2);
}
/**
* 金额乘法(元,如:金额*折扣)
*/
public static BigDecimal multiply(BigDecimal money, BigDecimal rate) {
return money.multiply(rate).setScale(MONEY_SCALE, ROUND_MODE);
}
/**
* 金额除法(元,如:金额/数量)
*/
public static BigDecimal divide(BigDecimal money, int count) {
if (count == 0) {
log.warn("金额除法,除数为0");
return BigDecimal.ZERO;
}
return money.divide(new BigDecimal(count), MONEY_SCALE, ROUND_MODE);
}
/**
* 金额比较(m1 >= m2 返回true)
*/
public static boolean ge(BigDecimal m1, BigDecimal m2) {
return m1.compareTo(m2) >= 0;
}
/**
* 金额比较(m1 > m2 返回true)
*/
public static boolean gt(BigDecimal m1, BigDecimal m2) {
return m1.compareTo(m2) > 0;
}
/**
* 金额比较(m1 <= m2 返回true)
*/
public static boolean le(BigDecimal m1, BigDecimal m2) {
return m1.compareTo(m2) <= 0;
}
/**
* 金额比较(m1 < m2 返回true)
*/
public static boolean lt(BigDecimal m1, BigDecimal m2) {
return m1.compareTo(m2) < 0;
}
/**
* 金额格式化(保留2位小数,补零)
*/
public static String format(BigDecimal money) {
return money.setScale(MONEY_SCALE, ROUND_MODE).toString();
}
/**
* 校验金额是否为正数
*/
public static boolean isPositive(BigDecimal money) {
return NumberUtil.isPositive(money);
}
/**
* 校验金额是否为非负数
*/
public static boolean isNonNegative(BigDecimal money) {
return NumberUtil.isNonNegative(money);
}
}
2.5 CacheUtil(本地缓存工具)
java
package com.xxx.common.business.util;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.xxx.common.exception.SystemException;
import com.xxx.common.enums.ErrorCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* 本地缓存工具(基于Caffeine,支持过期时间、自动刷新)
* 常用度:95%
* 所属层级:业务层/DAO层
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Component
public class CacheUtil {
/**
* 缓存容器(key:缓存名称,value:Caffeine缓存)
*/
private final Map<String, Cache<Object, Object>> cacheContainer = new ConcurrentHashMap<>();
/**
* 默认缓存配置
*/
private static final int DEFAULT_EXPIRE_TIME = 30; // 分钟
private static final int DEFAULT_MAX_SIZE = 10000;
/**
* 初始化默认缓存
*/
@PostConstruct
public void init() {
// 初始化通用缓存
createCache("commonCache", DEFAULT_EXPIRE_TIME, DEFAULT_MAX_SIZE);
log.info("本地缓存工具初始化完成");
}
/**
* 创建缓存
*
* @param cacheName 缓存名称
* @param expireTime 过期时间(分钟)
* @param maxSize 最大容量
*/
public void createCache(String cacheName, int expireTime, int maxSize) {
Cache<Object, Object> cache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireTime, TimeUnit.MINUTES)
.recordStats() // 记录缓存统计信息
.removalListener((key, value, cause) ->
log.debug("缓存[key={}]被移除,原因:{}", key, cause.name()))
.build();
cacheContainer.put(cacheName, cache);
log.info("创建缓存成功,名称:{},过期时间:{}分钟,最大容量:{}", cacheName, expireTime, maxSize);
}
/**
* 创建带自动刷新的缓存
*
* @param cacheName 缓存名称
* @param expireTime 过期时间(分钟)
* @param refreshTime 刷新时间(分钟)
* @param maxSize 最大容量
*/
public void createRefreshCache(String cacheName, int expireTime, int refreshTime, int maxSize) {
Cache<Object, Object> cache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireTime, TimeUnit.MINUTES)
.refreshAfterWrite(refreshTime, TimeUnit.MINUTES)
.recordStats()
.removalListener((key, value, cause) ->
log.debug("缓存[key={}]被移除,原因:{}", key, cause.name()))
.build();
cacheContainer.put(cacheName, cache);
log.info("创建自动刷新缓存成功,名称:{},过期时间:{}分钟,刷新时间:{}分钟,最大容量:{}",
cacheName, expireTime, refreshTime, maxSize);
}
/**
* 获取缓存(不存在则创建)
*/
private Cache<Object, Object> getCache(String cacheName) {
return cacheContainer.computeIfAbsent(cacheName, k -> {
log.warn("缓存[{}]不存在,创建默认配置缓存", k);
return Caffeine.newBuilder()
.maximumSize(DEFAULT_MAX_SIZE)
.expireAfterWrite(DEFAULT_EXPIRE_TIME, TimeUnit.MINUTES)
.build();
});
}
/**
* 添加缓存
*/
public void put(String cacheName, Object key, Object value) {
if (key == null) {
log.warn("缓存key不能为空");
return;
}
getCache(cacheName).put(key, value);
}
/**
* 获取缓存(无则返回null)
*/
@SuppressWarnings("unchecked")
public <T> T get(String cacheName, Object key) {
if (key == null) {
return null;
}
return (T) getCache(cacheName).getIfPresent(key);
}
/**
* 获取缓存(无则加载)
*/
@SuppressWarnings("unchecked")
public <T> T get(String cacheName, Object key, Function<Object, T> loader) {
if (key == null) {
throw new SystemException(ErrorCodeEnum.PARAM_INVALID, "缓存key不能为空");
}
try {
return (T) getCache(cacheName).get(key, k -> loader.apply(k));
} catch (Exception e) {
log.error("缓存加载失败,cacheName={}, key={}", cacheName, key, e);
throw new SystemException(ErrorCodeEnum.SYSTEM_ERROR, "缓存加载失败");
}
}
/**
* 删除缓存
*/
public void remove(String cacheName, Object key) {
if (key == null) {
return;
}
getCache(cacheName).invalidate(key);
}
/**
* 清空缓存
*/
public void clear(String cacheName) {
getCache(cacheName).invalidateAll();
log.info("清空缓存成功,名称:{}", cacheName);
}
/**
* 获取缓存统计信息
*/
public String getStats(String cacheName) {
return getCache(cacheName).stats().toString();
}
/**
* 获取通用缓存(默认缓存)
*/
public <T> T getCommonCache(Object key) {
return get("commonCache", key);
}
/**
* 添加通用缓存
*/
public void putCommonCache(Object key, Object value) {
put("commonCache", key, value);
}
}
四、安全防护模块(上线前必用,95%通用)
1. 安全工具类
1.1 XssFilter(防XSS攻击过滤器)
java
package com.xxx.common.security.filter;
import com.xxx.common.util.StringUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
import java.util.regex.Pattern;
/**
* XSS攻击防护过滤器(过滤请求参数中的脚本,支持白名单)
* 常用度:95%
* 所属层级:控制层(过滤器)
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Component
@WebFilter(urlPatterns = "/*")
@Order(1) // 优先级最高
public class XssFilter implements Filter {
/**
* XSS白名单接口(逗号分隔)
*/
@Value("${security.xss.whitelist:/api/v1/public/*,/swagger-ui/**}")
private String xssWhitelist;
private List<String> whitelistPatterns;
/**
* XSS过滤正则
*/
private static final Pattern XSS_PATTERN = Pattern.compile(
"<script.*?>.*?</script.*?>|javascript:|onload=|onclick=|onmouseover=|onerror=|onfocus=|eval\\(|alert\\(|confirm\\(|prompt\\(|iframe|frame|script",
Pattern.CASE_INSENSITIVE
);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化白名单
whitelistPatterns = List.of(xssWhitelist.split(","));
log.info("XSS过滤器初始化完成,白名单:{}", whitelistPatterns);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 检查是否在白名单中
String requestURI = httpRequest.getRequestURI();
if (isWhitelist(requestURI)) {
chain.doFilter(request, response);
return;
}
// 包装请求,过滤参数
XssHttpServletRequestWrapper wrappedRequest = new XssHttpServletRequestWrapper(httpRequest);
chain.doFilter(wrappedRequest, httpResponse);
}
/**
* 判断是否在白名单中
*/
private boolean isWhitelist(String requestURI) {
for (String pattern : whitelistPatterns) {
if (StringUtil.isBlank(pattern)) {
continue;
}
// 支持通配符*
String regex = pattern.replace("*", ".*");
if (requestURI.matches(regex)) {
return true;
}
}
return false;
}
/**
* XSS请求包装器
*/
private static class XssHttpServletRequestWrapper extends jakarta.servlet.http.HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return filterXss(value);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) {
return null;
}
for (int i = 0; i < values.length; i++) {
values[i] = filterXss(values[i]);
}
return values;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return filterXss(value);
}
/**
* XSS过滤
*/
private String filterXss(String value) {
if (StringUtil.isBlank(value)) {
return value;
}
// 替换危险字符
value = XSS_PATTERN.matcher(value).replaceAll("");
// 转义HTML特殊字符
value = value.replace("<", "<")
.replace(">", ">")
.replace("&", "&")
.replace("\"", """)
.replace("'", "'");
return value;
}
}
@Override
public void destroy() {
log.info("XSS过滤器销毁");
}
}
1.2 SensitiveUtil(敏感信息脱敏工具)
java
package com.xxx.common.security.util;
import com.xxx.common.util.StringUtil;
/**
* 敏感信息脱敏工具(统一脱敏规则)
* 常用度:95%
* 所属层级:通用层(控制/业务)
*
* @author xxx
* @date 2025-11-30
*/
public class SensitiveUtil {
/**
* 手机号脱敏(138****1234)
*/
public static String maskMobile(String mobile) {
return StringUtil.maskMobile(mobile);
}
/**
* 身份证脱敏(110101********1234)
*/
public static String maskIdCard(String idCard) {
return StringUtil.maskIdCard(idCard);
}
/**
* 银行卡脱敏(6226****1234)
*/
public static String maskBankCard(String bankCard) {
return StringUtil.maskBankCard(bankCard);
}
/**
* 姓名脱敏(张*三 / 李*)
*/
public static String maskName(String name) {
return StringUtil.maskName(name);
}
/**
* 邮箱脱敏(z***@xxx.com)
*/
public static String maskEmail(String email) {
if (StringUtil.isBlank(email) || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String prefix = parts[0];
String suffix = parts[1];
if (prefix.length() <= 1) {
return "*@" + suffix;
} else if (prefix.length() == 2) {
return prefix.substring(0, 1) + "*@" + suffix;
} else {
return prefix.substring(0, 1) + "***" + prefix.substring(prefix.length() - 1) + "@" + suffix;
}
}
/**
* 密码脱敏(全部替换为*)
*/
public static String maskPassword(String password) {
if (StringUtil.isBlank(password)) {
return "";
}
return "******";
}
/**
* 地址脱敏(北京市朝阳区******)
*/
public static String maskAddress(String address) {
if (StringUtil.isBlank(address)) {
return address;
}
int length = address.length();
if (length <= 6) {
return address.substring(0, 3) + "***";
} else {
return address.substring(0, 6) + "******";
}
}
/**
* 自定义脱敏(保留前prefixLen位,后suffixLen位,中间用*填充)
*/
public static String maskCustom(String content, int prefixLen, int suffixLen) {
if (StringUtil.isBlank(content)) {
return content;
}
int length = content.length();
if (length <= prefixLen + suffixLen) {
return content.substring(0, prefixLen) + "*".repeat(length - prefixLen);
}
String prefix = content.substring(0, prefixLen);
String suffix = content.substring(length - suffixLen);
return prefix + "*".repeat(length - prefixLen - suffixLen) + suffix;
}
}
1.3 TokenUtil(JWT工具类)
java
package com.xxx.common.security.util;
import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.util.DateUtil;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
/**
* JWT Token工具类(生成/解析/刷新)
* 常用度:95%
* 所属层级:控制层/业务层
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Component
public class TokenUtil {
/**
* JWT密钥(生产环境从配置中心读取,长度至少32位)
*/
@Value("${security.jwt.secret:xxx1234567890abcdefxxx1234567890abc}")
private String jwtSecret;
/**
* Token过期时间(分钟)
*/
@Value("${security.jwt.expire-time:120}")
private int expireTime;
/**
* 刷新Token过期时间(天)
*/
@Value("${security.jwt.refresh-expire-time:7}")
private int refreshExpireTime;
/**
* 生成访问Token
*/
public String generateToken(String userId, Map<String, Object> claims) {
// 生成密钥
SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
// 过期时间
Date expireDate = new Date(System.currentTimeMillis() + expireTime * 60 * 1000L);
// 构建Token
return Jwts.builder()
.setClaims(claims) // 自定义载荷
.setSubject(userId) // 主题(用户ID)
.setIssuedAt(new Date()) // 签发时间
.setExpiration(expireDate) // 过期时间
.signWith(key, SignatureAlgorithm.HS256) // 签名算法
.compact();
}
/**
* 生成刷新Token
*/
public String generateRefreshToken(String userId) {
SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
Date expireDate = new Date(System.currentTimeMillis() + refreshExpireTime * 24 * 60 * 1000L);
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(new Date())
.setExpiration(expireDate)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 解析Token
*/
public Claims parseToken(String token) {
try {
SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
log.error("Token已过期", e);
throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "Token已过期");
} catch (UnsupportedJwtException e) {
log.error("不支持的Token格式", e);
throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "不支持的Token格式");
} catch (MalformedJwtException e) {
log.error("Token格式错误", e);
throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "Token格式错误");
} catch (SignatureException e) {
log.error("Token签名验证失败", e);
throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "Token签名验证失败");
} catch (IllegalArgumentException e) {
log.error("Token为空或无效", e);
throw new BusinessException(ErrorCodeEnum.TOKEN_MISSING, "Token不能为空");
} catch (Exception e) {
log.error("Token解析失败", e);
throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "Token解析失败");
}
}
/**
* 验证Token是否有效
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 从Token中获取用户ID
*/
public String getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
/**
* 从Token中获取自定义字段
*/
public <T> T getClaimFromToken(String token, String claimName, Class<T> clazz) {
Claims claims = parseToken(token);
return claims.get(claimName, clazz);
}
/**
* 刷新Token(原Token未过期时刷新)
*/
public String refreshToken(String oldToken) {
Claims claims = parseToken(oldToken);
// 检查原Token剩余有效期(至少剩余5分钟才允许刷新)
Date expireDate = claims.getExpiration();
long remainTime = expireDate.getTime() - System.currentTimeMillis();
if (remainTime < 5 * 60 * 1000L) {
throw new BusinessException(ErrorCodeEnum.TOKEN_INVALID, "Token即将过期,无法刷新");
}
// 生成新Token
return generateToken(claims.getSubject(), claims);
}
/**
* 获取Token过期时间
*/
public String getTokenExpireTime(String token) {
Claims claims = parseToken(token);
return DateUtil.format(DateUtil.dateToLocalDateTime(claims.getExpiration()));
}
}
1.4 SignUtil(接口签名工具)
java
package com.xxx.common.security.util;
import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
/**
* 接口签名工具(防止参数篡改,支持超时校验)
* 常用度:95%
* 所属层级:控制层/业务层
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Component
public class SignUtil {
/**
* 签名密钥(生产环境从配置中心读取)
*/
@Value("${security.sign.secret:xxx_sign_key_2025}")
private String signSecret;
/**
* 签名超时时间(秒)
*/
@Value("${security.sign.timeout:5}")
private int signTimeout;
/**
* 生成签名
*
* @param params 请求参数(不含sign)
* @param timestamp 时间戳(毫秒)
* @return 签名
*/
public String generateSign(Map<String, Object> params, long timestamp) {
// 1. 参数排序(按key升序)
List<String> sortedKeys = new ArrayList<>(params.keySet());
Collections.sort(sortedKeys);
// 2. 拼接参数字符串(key=value&key=value)
StringBuilder sb = new StringBuilder();
for (String key : sortedKeys) {
Object value = params.get(key);
if (value == null || StringUtil.isBlank(value.toString())) {
continue;
}
sb.append(key).append("=").append(value).append("&");
}
// 3. 拼接时间戳和密钥
sb.append("timestamp=").append(timestamp).append("&secret=").append(signSecret);
// 4. MD5加密
return md5(sb.toString());
}
/**
* 验证签名
*
* @param params 请求参数(含sign)
* @param sign 客户端传入的签名
* @return 是否有效
*/
public boolean verifySign(Map<String, Object> params, String sign) {
// 1. 复制参数,移除sign字段
Map<String, Object> copyParams = new HashMap<>(params);
copyParams.remove("sign");
// 2. 获取时间戳
Object timestampObj = copyParams.get("timestamp");
if (timestampObj == null) {
log.warn("签名验证失败:缺少timestamp参数");
return false;
}
long timestamp;
try {
timestamp = Long.parseLong(timestampObj.toString());
} catch (NumberFormatException e) {
log.warn("签名验证失败:timestamp格式错误");
return false;
}
// 3. 校验超时
long currentTime = System.currentTimeMillis();
long diff = Math.abs(currentTime - timestamp);
if (diff > signTimeout * 1000L) {
log.warn("签名验证失败:请求超时,timestamp={}, currentTime={}, diff={}ms",
timestamp, currentTime, diff);
return false;
}
// 4. 生成服务端签名
String serverSign = generateSign(copyParams, timestamp);
// 5. 对比签名
boolean valid = serverSign.equals(sign);
if (!valid) {
log.warn("签名验证失败:客户端签名={}, 服务端签名={}", sign, serverSign);
}
return valid;
}
/**
* 验证签名(抛出异常)
*/
public void validateSign(Map<String, Object> params, String sign) {
if (!verifySign(params, sign)) {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "签名验证失败");
}
}
/**
* MD5加密
*/
private String md5(String content) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(content.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
sb.append("0");
}
sb.append(hex);
}
return sb.toString().toUpperCase();
} catch (NoSuchAlgorithmException e) {
log.error("MD5加密失败", e);
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "签名生成失败");
}
}
}
2. 防护组件
2.1 IdempotentAOP(幂等性注解AOP)
java
package com.xxx.common.security.aop;
import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 幂等性注解AOP(基于Redis,支持SpEL表达式)
* 常用度:95%
* 所属层级:业务层(AOP)
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Aspect
@Component
public class IdempotentAOP {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 幂等性注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* 幂等键(SpEL表达式,如:#orderNo、#user.id)
*/
String key();
/**
* 过期时间(秒)
*/
int expireTime() default 60;
/**
* 重复提交提示语
*/
String message() default "请勿重复提交";
}
/**
* 切点
*/
@Pointcut("@annotation(com.xxx.common.security.aop.IdempotentAOP.Idempotent)")
public void idempotentPointcut() {
}
/**
* 环绕通知
*/
@Around("idempotentPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取注解信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Idempotent idempotent = signature.getMethod().getAnnotation(Idempotent.class);
String keyExpression = idempotent.key();
int expireTime = idempotent.expireTime();
String message = idempotent.message();
// 解析SpEL表达式,生成幂等键
String idempotentKey = parseSpEL(keyExpression, joinPoint);
if (StringUtil.isBlank(idempotentKey)) {
throw new BusinessException(ErrorCodeEnum.PARAM_INVALID, "幂等键不能为空");
}
String redisKey = "idempotent:" + idempotentKey;
// 尝试设置Redis锁
Boolean success = redisTemplate.opsForValue().setIfAbsent(redisKey, "1", expireTime, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(success)) {
log.warn("幂等性校验失败,重复请求,key:{}", redisKey);
throw new BusinessException(ErrorCodeEnum.BUSINESS_COMMON_ERROR, message);
}
try {
// 执行目标方法
return joinPoint.proceed();
} catch (Exception e) {
// 业务异常,释放锁
redisTemplate.delete(redisKey);
throw e;
}
}
/**
* 解析SpEL表达式
*/
private String parseSpEL(String expression, ProceedingJoinPoint joinPoint) {
// 获取方法参数
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] paramNames = signature.getParameterNames();
Object[] args = joinPoint.getArgs();
// 构建SpEL上下文
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
// 解析表达式
ExpressionParser parser = new SpelExpressionParser();
Object value = parser.parseExpression(expression).getValue(context);
return value == null ? "" : value.toString();
}
}
2.2 AntiBrushInterceptor(接口防刷拦截器)
java
package com.xxx.common.security.interceptor;
import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.util.StringUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 接口防刷拦截器(按IP+接口限流,支持黑名单、阈值配置)
* 常用度:95%
* 所属层级:控制层(拦截器)
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Component
public class AntiBrushInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 防刷白名单接口(逗号分隔,支持通配符*)
*/
@Value("${security.anti-brush.whitelist:/api/v1/public/*,/swagger-ui/**,/actuator/**}")
private String antiBrushWhitelist;
/**
* 默认限流阈值(单位时间内最大请求数)
*/
@Value("${security.anti-brush.default-limit:60}")
private int defaultLimit;
/**
* 限流时间窗口(秒)
*/
@Value("${security.anti-brush.time-window:60}")
private int timeWindow;
/**
* 黑名单前缀
*/
private static final String BLACKLIST_KEY_PREFIX = "anti_brush:blacklist:";
/**
* 限流计数前缀
*/
private static final String LIMIT_KEY_PREFIX = "anti_brush:limit:";
/**
* 黑名单过期时间(小时)
*/
private static final int BLACKLIST_EXPIRE_HOURS = 24;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String ip = getIpAddress(request);
// 1. 检查IP是否在黑名单
if (isBlacklisted(ip)) {
log.warn("IP[{}]访问接口[{}]被拦截:已加入黑名单", ip, requestURI);
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "请求过于频繁,请24小时后重试");
}
// 2. 检查是否在白名单
if (isWhitelist(requestURI)) {
return true;
}
// 3. 接口限流校验
String limitKey = LIMIT_KEY_PREFIX + ip + ":" + requestURI;
Long count = redisTemplate.opsForValue().increment(limitKey, 1);
// 首次访问,设置过期时间
if (count != null && count == 1) {
redisTemplate.expire(limitKey, timeWindow, TimeUnit.SECONDS);
}
// 4. 超过阈值,加入黑名单
if (count != null && count > defaultLimit) {
addToBlacklist(ip);
log.warn("IP[{}]访问接口[{}]触发限流:请求次数={},阈值={}", ip, requestURI, count, defaultLimit);
throw new BusinessException(ErrorCodeEnum.SYSTEM_ERROR, "请求过于频繁,请稍后重试");
}
return true;
}
/**
* 获取客户端真实IP
*/
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 处理多IP场景(X-Forwarded-For可能包含多个IP,取第一个)
if (StringUtil.isNotBlank(ip) && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return StringUtil.isBlank(ip) ? "127.0.0.1" : ip;
}
/**
* 判断是否在白名单
*/
private boolean isWhitelist(String requestURI) {
List<String> whitelist = List.of(antiBrushWhitelist.split(","));
for (String pattern : whitelist) {
if (StringUtil.isBlank(pattern)) {
continue;
}
String regex = pattern.replace("*", ".*");
if (requestURI.matches(regex)) {
return true;
}
}
return false;
}
/**
* 判断IP是否在黑名单
*/
private boolean isBlacklisted(String ip) {
String blacklistKey = BLACKLIST_KEY_PREFIX + ip;
return Boolean.TRUE.equals(redisTemplate.hasKey(blacklistKey));
}
/**
* 将IP加入黑名单
*/
private void addToBlacklist(String ip) {
String blacklistKey = BLACKLIST_KEY_PREFIX + ip;
redisTemplate.opsForValue().set(blacklistKey, "1", BLACKLIST_EXPIRE_HOURS, TimeUnit.HOURS);
log.info("IP[{}]被加入防刷黑名单,过期时间:{}小时", ip, BLACKLIST_EXPIRE_HOURS);
}
/**
* 移除黑名单IP(手动解除)
*/
public void removeBlacklist(String ip) {
String blacklistKey = BLACKLIST_KEY_PREFIX + ip;
redisTemplate.delete(blacklistKey);
log.info("IP[{}]被移出防刷黑名单", ip);
}
/**
* 自定义接口限流阈值(覆盖默认值)
*/
public void setInterfaceLimit(String requestURI, int limit) {
String limitConfigKey = "anti_brush:config:" + requestURI;
redisTemplate.opsForValue().set(limitConfigKey, limit, 7, TimeUnit.DAYS);
log.info("接口[{}]限流阈值设置为:{}", requestURI, limit);
}
}
// 拦截器配置类
package com.xxx.common.config;
import com.xxx.common.security.interceptor.AntiBrushInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器注册配置
* 常用度:95%
* 所属层级:配置层
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private AntiBrushInterceptor antiBrushInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册接口防刷拦截器
registry.addInterceptor(antiBrushInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/swagger-ui/**", "/v3/api-docs/**", "/actuator/**");
}
}
2.3 SqlInjectFilter(防SQL注入过滤器)
java
package com.xxx.common.security.filter;
import com.xxx.common.util.StringUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
import java.util.regex.Pattern;
/**
* 防SQL注入过滤器(过滤危险关键字,支持白名单)
* 常用度:95%
* 所属层级:控制层(过滤器)
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Component
@WebFilter(urlPatterns = "/*")
@Order(2) // 优先级低于XSS过滤器
public class SqlInjectFilter implements Filter {
/**
* SQL注入白名单接口
*/
@Value("${security.sql-inject.whitelist:/api/v1/public/*,/swagger-ui/**,/actuator/**}")
private String sqlInjectWhitelist;
/**
* SQL注入危险关键字正则(忽略大小写)
*/
private static final Pattern SQL_INJECT_PATTERN = Pattern.compile(
"(\\b(ALTER|CREATE|DELETE|DROP|EXEC|INSERT|MERGE|SELECT|UPDATE|UNION|AND|OR|NOT|WHERE)\\b)|(--)|(;)|(\\|\\|)|(\\bEXECUTE\\b)|(\\bDECLARE\\b)",
Pattern.CASE_INSENSITIVE
);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
// 检查白名单
if (isWhitelist(requestURI)) {
chain.doFilter(request, response);
return;
}
// 包装请求,过滤SQL注入关键字
SqlInjectRequestWrapper wrappedRequest = new SqlInjectRequestWrapper(httpRequest);
chain.doFilter(wrappedRequest, response);
}
/**
* 判断是否在白名单
*/
private boolean isWhitelist(String requestURI) {
List<String> whitelist = List.of(sqlInjectWhitelist.split(","));
for (String pattern : whitelist) {
if (StringUtil.isBlank(pattern)) {
continue;
}
String regex = pattern.replace("*", ".*");
if (requestURI.matches(regex)) {
return true;
}
}
return false;
}
/**
* SQL注入请求包装器
*/
private static class SqlInjectRequestWrapper extends jakarta.servlet.http.HttpServletRequestWrapper {
public SqlInjectRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return filterSqlInject(value);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) {
return null;
}
for (int i = 0; i < values.length; i++) {
values[i] = filterSqlInject(values[i]);
}
return values;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return filterSqlInject(value);
}
/**
* 过滤SQL注入关键字
*/
private String filterSqlInject(String value) {
if (StringUtil.isBlank(value)) {
return value;
}
// 替换危险关键字为空格
value = SQL_INJECT_PATTERN.matcher(value).replaceAll(" ");
log.debug("SQL注入过滤后的值:{}", value);
return value;
}
}
}
五、监控审计模块(上线前必用,90%通用)
1. 日志审计
1.1 OperateLogAOP(操作日志AOP)
java
package com.xxx.common.monitor.aop;
import com.xxx.common.util.TraceIdUtil;
import com.xxx.common.util.StringUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.annotation.*;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 操作日志AOP(注解式记录,支持自定义描述)
* 常用度:90%
* 所属层级:通用层(AOP)
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Aspect
@Component
public class OperateLogAOP {
@Autowired(required = false)
private HttpServletRequest request;
/**
* 操作日志注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
/**
* 操作描述
*/
String value() default "";
/**
* 操作类型(如:ADD/UPDATE/DELETE/QUERY)
*/
String type() default "UNKNOWN";
/**
* 是否记录请求参数
*/
boolean recordParams() default true;
/**
* 是否记录返回结果
*/
boolean recordResult() default false;
}
/**
* 切点:所有标注@OperateLog的方法
*/
@Pointcut("@annotation(com.xxx.common.monitor.aop.OperateLogAOP.OperateLog)")
public void operateLogPointcut() {
}
/**
* 环绕通知:记录操作日志
*/
@Around("operateLogPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 获取注解信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
OperateLog operateLog = signature.getMethod().getAnnotation(OperateLog.class);
String desc = operateLog.value();
String type = operateLog.type();
boolean recordParams = operateLog.recordParams();
boolean recordResult = operateLog.recordResult();
// 2. 构建日志上下文
Map<String, Object> logContext = new HashMap<>();
logContext.put("traceId", TraceIdUtil.getTraceId());
logContext.put("operateTime", LocalDateTime.now());
logContext.put("operateDesc", desc);
logContext.put("operateType", type);
logContext.put("className", joinPoint.getTarget().getClass().getName());
logContext.put("methodName", signature.getMethod().getName());
// 3. 获取请求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
logContext.put("requestURI", request.getRequestURI());
logContext.put("requestMethod", request.getMethod());
logContext.put("clientIp", getClientIp(request));
logContext.put("userAgent", request.getHeader("User-Agent"));
}
// 4. 记录请求参数
if (recordParams) {
String[] paramNames = signature.getParameterNames();
Object[] args = joinPoint.getArgs();
Map<String, Object> params = new HashMap<>();
for (int i = 0; i < paramNames.length; i++) {
// 敏感参数脱敏(如:password、mobile)
Object value = args[i];
if (paramNames[i].toLowerCase().contains("password")) {
value = "******";
} else if (paramNames[i].toLowerCase().contains("mobile")) {
value = StringUtil.maskMobile(value.toString());
}
params.put(paramNames[i], value);
}
logContext.put("requestParams", params);
}
long startTime = System.currentTimeMillis();
Object result = null;
boolean success = true;
String errorMsg = "";
try {
// 5. 执行目标方法
result = joinPoint.proceed();
return result;
} catch (Exception e) {
success = false;
errorMsg = e.getMessage();
throw e;
} finally {
// 6. 记录日志
long costTime = System.currentTimeMillis() - startTime;
logContext.put("costTime", costTime + "ms");
logContext.put("success", success);
logContext.put("errorMsg", errorMsg);
if (recordResult && success) {
logContext.put("responseResult", result);
}
// 7. 输出结构化日志(JSON格式)
log.info("【操作日志】{}", logContext);
// 扩展:可将日志写入数据库/ES等存储介质
// saveOperateLog(logContext);
}
}
/**
* 获取客户端IP
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return StringUtil.isBlank(ip) ? "127.0.0.1" : ip.split(",")[0].trim();
}
/**
* 保存操作日志到存储介质(示例,需根据业务实现)
*/
private void saveOperateLog(Map<String, Object> logContext) {
// 此处可实现日志入库/入ES等逻辑
// log.info("保存操作日志到数据库:{}", logContext);
}
}
1.2 TraceIdUtil(全链路TraceId工具)
java
package com.xxx.common.util;
import org.slf4j.MDC;
import java.util.UUID;
/**
* 全链路TraceId工具(生成/传递TraceId,放入MDC)
* 常用度:90%
* 所属层级:通用层
*
* @author xxx
* @date 2025-11-30
*/
public class TraceIdUtil {
/**
* TraceId MDC键名
*/
private static final String TRACE_ID_KEY = "traceId";
/**
* 生成TraceId(UUID去除横线)
*/
public static String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 获取当前TraceId(MDC中)
*/
public static String getTraceId() {
String traceId = MDC.get(TRACE_ID_KEY);
if (StringUtil.isBlank(traceId)) {
traceId = generateTraceId();
MDC.put(TRACE_ID_KEY, traceId);
}
return traceId;
}
/**
* 设置TraceId到MDC
*/
public static void setTraceId(String traceId) {
if (StringUtil.isNotBlank(traceId)) {
MDC.put(TRACE_ID_KEY, traceId);
}
}
/**
* 清除MDC中的TraceId
*/
public static void clearTraceId() {
MDC.remove(TRACE_ID_KEY);
}
/**
* 从请求头获取TraceId(跨服务传递)
*/
public static String getTraceIdFromHeader(jakarta.servlet.http.HttpServletRequest request) {
String traceId = request.getHeader("X-Trace-Id");
if (StringUtil.isBlank(traceId)) {
traceId = request.getParameter("traceId");
}
return StringUtil.isNotBlank(traceId) ? traceId : generateTraceId();
}
}
// TraceId过滤器(请求入口生成/传递TraceId)
package com.xxx.common.monitor.filter;
import com.xxx.common.util.TraceIdUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* TraceId过滤器(请求入口初始化TraceId)
* 常用度:90%
* 所属层级:控制层(过滤器)
*/
@Slf4j
@Component
@WebFilter(urlPatterns = "/*")
@Order(0) // 优先级最高
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 从请求头获取TraceId,无则生成
String traceId = TraceIdUtil.getTraceIdFromHeader(httpRequest);
TraceIdUtil.setTraceId(traceId);
// 响应头返回TraceId
((jakarta.servlet.http.HttpServletResponse) response).setHeader("X-Trace-Id", traceId);
chain.doFilter(request, response);
} finally {
// 清除MDC,避免线程复用导致污染
TraceIdUtil.clearTraceId();
}
}
}
1.3 LogFormatter(日志格式化工具)
java
package com.xxx.common.monitor.util;
import com.alibaba.fastjson2.JSON;
import com.xxx.common.enums.EnvEnum;
import com.xxx.common.util.StringUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
* 日志格式化工具(统一输出格式,支持JSON/文本切换)
* 常用度:90%
* 所属层级:通用层
*
* @author xxx
* @date 2025-11-30
*/
@Component
public class LogFormatter {
/**
* 日志输出格式(json/text)
*/
@Value("${logging.format.type:json}")
private String logFormatType;
/**
* 环境标识
*/
@Value("${spring.profiles.active:dev}")
private String env;
/**
* 应用名称
*/
@Value("${spring.application.name:unknown-app}")
private String appName;
/**
* 通用日期格式
*/
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
/**
* 格式化日志(通用方法)
*/
public String format(String level, String traceId, String module, String message, Object... args) {
// 格式化消息参数
String formattedMsg = StringUtil.isBlank(message) ? "" : String.format(message, args);
// 构建日志上下文
Map<String, Object> logMap = new HashMap<>();
logMap.put("timestamp", LocalDateTime.now().format(DATE_FORMATTER));
logMap.put("level", level);
logMap.put("traceId", traceId);
logMap.put("module", module);
logMap.put("appName", appName);
logMap.put("env", env);
logMap.put("message", formattedMsg);
// 生产环境隐藏敏感信息
if (EnvEnum.getByCode(env).isProd()) {
// 脱敏处理
logMap.put("message", maskSensitiveInfo(formattedMsg));
}
// 按格式输出
if ("json".equalsIgnoreCase(logFormatType)) {
return JSON.toJSONString(logMap);
} else {
return String.format("[%s] [%s] [%s] [%s] [%s] %s",
logMap.get("timestamp"),
logMap.get("level"),
logMap.get("appName"),
logMap.get("traceId"),
logMap.get("module"),
logMap.get("message"));
}
}
/**
* 格式化业务日志
*/
public String formatBusinessLog(String traceId, String module, String message, Object... args) {
return format("INFO", traceId, module, message, args);
}
/**
* 格式化异常日志
*/
public String formatExceptionLog(String traceId, String module, String message, Throwable e) {
Map<String, Object> exceptionMap = new HashMap<>();
exceptionMap.put("exceptionType", e.getClass().getName());
exceptionMap.put("exceptionMsg", e.getMessage());
// 生产环境不打印堆栈
if (!EnvEnum.getByCode(env).isProd()) {
exceptionMap.put("stackTrace", getStackTrace(e));
}
String exceptionMsg = JSON.toJSONString(exceptionMap);
return format("ERROR", traceId, module, message + " | 异常信息:%s", exceptionMsg);
}
/**
* 敏感信息脱敏
*/
private String maskSensitiveInfo(String message) {
if (StringUtil.isBlank(message)) {
return message;
}
// 手机号脱敏
message = message.replaceAll("(1[3-9]\\d{9})", "$1".replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
// 身份证脱敏
message = message.replaceAll("([1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx])",
"$1".replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2"));
// 银行卡脱敏
message = message.replaceAll("(\\d{16,19})", "$1".replaceAll("(\\d{4})\\d+(\\d{4})", "$1****$2"));
return message;
}
/**
* 获取异常堆栈信息
*/
private String getStackTrace(Throwable e) {
StringBuilder sb = new StringBuilder();
for (StackTraceElement element : e.getStackTrace()) {
sb.append(element.toString()).append("\n");
}
return sb.toString();
}
}
2. 监控工具
2.1 MetricsUtil(自定义指标工具)
java
package com.xxx.common.monitor.util;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.tag.Tag;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 自定义指标工具(适配Prometheus,记录接口耗时、调用次数)
* 常用度:90%
* 所属层级:通用层(控制/业务)
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Component
public class MetricsUtil {
@Autowired
private MeterRegistry meterRegistry;
@Autowired(required = false)
private PrometheusMeterRegistry prometheusMeterRegistry;
/**
* 计数器缓存(避免重复创建)
*/
private final Map<String, Counter> counterCache = new ConcurrentHashMap<>();
/**
* 计时器缓存
*/
private final Map<String, Timer> timerCache = new ConcurrentHashMap<>();
/**
* 通用标签:应用名称
*/
private static final String APP_TAG_KEY = "app";
/**
* 通用标签:接口名称
*/
private static final String API_TAG_KEY = "api";
/**
* 通用标签:结果(success/fail)
*/
private static final String RESULT_TAG_KEY = "result";
/**
* 递增计数器(接口调用次数)
*/
public void incrementApiCounter(String apiName, boolean success) {
String counterName = "api_call_count";
String result = success ? "success" : "fail";
// 构建标签
Tag appTag = Tag.of(APP_TAG_KEY, getAppName());
Tag apiTag = Tag.of(API_TAG_KEY, apiName);
Tag resultTag = Tag.of(RESULT_TAG_KEY, result);
// 获取或创建计数器
String cacheKey = counterName + "_" + apiName + "_" + result;
Counter counter = counterCache.computeIfAbsent(cacheKey, k ->
Counter.builder(counterName)
.description("接口调用次数")
.tags(appTag, apiTag, resultTag)
.register(meterRegistry)
);
// 递增
counter.increment();
log.debug("计数器[{}]递增,api={},result={}", counterName, apiName, result);
}
/**
* 记录接口耗时
*/
public void recordApiTimer(String apiName, long duration, TimeUnit timeUnit) {
String timerName = "api_call_duration_seconds";
// 构建标签
Tag appTag = Tag.of(APP_TAG_KEY, getAppName());
Tag apiTag = Tag.of(API_TAG_KEY, apiName);
// 获取或创建计时器
String cacheKey = timerName + "_" + apiName;
Timer timer = timerCache.computeIfAbsent(cacheKey, k ->
Timer.builder(timerName)
.description("接口调用耗时(秒)")
.tags(appTag, apiTag)
.register(meterRegistry)
);
// 记录耗时
timer.record(duration, timeUnit);
log.debug("计时器[{}]记录耗时,api={},duration={}{}", timerName, apiName, duration, timeUnit.name());
}
/**
* 自定义计数器
*/
public void incrementCounter(String counterName, String... tagValues) {
Tag[] tags = buildTags(tagValues);
String cacheKey = counterName + "_" + String.join("_", tagValues);
Counter counter = counterCache.computeIfAbsent(cacheKey, k ->
Counter.builder(counterName)
.tags(tags)
.register(meterRegistry)
);
counter.increment();
}
/**
* 构建标签(键值对形式,如:"key1","value1","key2","value2")
*/
private Tag[] buildTags(String... tagValues) {
if (tagValues == null || tagValues.length % 2 != 0) {
log.warn("标签参数必须为键值对形式");
return new Tag[0];
}
Tag[] tags = new Tag[tagValues.length / 2];
for (int i = 0; i < tagValues.length; i += 2) {
tags[i / 2] = Tag.of(tagValues[i], tagValues[i + 1]);
}
// 添加默认应用标签
return Arrays.copyOf(tags, tags.length + 1, Tag[].class);
}
/**
* 获取应用名称(从环境变量/配置中读取)
*/
private String getAppName() {
return System.getProperty("spring.application.name", "unknown-app");
}
/**
* 导出Prometheus指标(用于自定义暴露)
*/
public String exportPrometheusMetrics() {
if (prometheusMeterRegistry == null) {
log.warn("未配置PrometheusMeterRegistry,无法导出指标");
return "";
}
return prometheusMeterRegistry.scrape();
}
}
// 接口耗时监控AOP(示例)
package com.xxx.common.monitor.aop;
import com.xxx.common.monitor.util.MetricsUtil;
import lombok.extern.slf4j.Slf4j;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 接口耗时监控AOP
* 常用度:90%
* 所属层级:控制层(AOP)
*/
@Slf4j
@Aspect
@Component
public class ApiMetricsAOP {
@Autowired
private MetricsUtil metricsUtil;
/**
* 切点:所有Controller层方法
*/
@Pointcut("within(com.xxx.api.controller..*)")
public void apiMetricsPointcut() {
}
@Around("apiMetricsPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String apiName = joinPoint.getSignature().toShortString();
long startTime = System.currentTimeMillis();
boolean success = true;
try {
return joinPoint.proceed();
} catch (Exception e) {
success = false;
throw e;
} finally {
// 记录调用次数
metricsUtil.incrementApiCounter(apiName, success);
// 记录耗时
long duration = System.currentTimeMillis() - startTime;
metricsUtil.recordApiTimer(apiName, duration, TimeUnit.MILLISECONDS);
}
}
}
2.2 HealthCheckUtil(健康检查工具)
java
package com.xxx.common.monitor.util;
import com.xxx.common.enums.ErrorCodeEnum;
import com.xxx.common.exception.SystemException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/**
* 健康检查工具(自定义检查项:Redis/DB连接状态)
* 常用度:90%
* 所属层级:通用层(DAO/业务)
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Component
public class HealthCheckUtil {
@Autowired(required = false)
private DataSource dataSource;
@Autowired(required = false)
private JdbcTemplate jdbcTemplate;
@Autowired(required = false)
private RedisTemplate<String, Object> redisTemplate;
/**
* 执行全量健康检查
*/
public Map<String, HealthCheckResult> checkAll() {
Map<String, HealthCheckResult> resultMap = new HashMap<>();
// 数据库检查
resultMap.put("database", checkDatabase());
// Redis检查
resultMap.put("redis", checkRedis());
// 自定义检查项(如:MinIO、消息队列等)
resultMap.put("custom", checkCustom());
return resultMap;
}
/**
* 数据库健康检查
*/
public HealthCheckResult checkDatabase() {
HealthCheckResult result = new HealthCheckResult();
result.setName("database");
try {
if (dataSource == null) {
result.setHealthy(false);
result.setMessage("数据源未配置");
return result;
}
// 测试数据库连接
try (Connection conn = dataSource.getConnection()) {
if (conn.isValid(3)) {
// 执行简单查询
jdbcTemplate.queryForObject("SELECT 1", Integer.class);
result.setHealthy(true);
result.setMessage("数据库连接正常");
} else {
result.setHealthy(false);
result.setMessage("数据库连接无效");
}
} catch (SQLException e) {
result.setHealthy(false);
result.setMessage("数据库连接失败:" + e.getMessage());
log.error("数据库健康检查失败", e);
}
} catch (Exception e) {
result.setHealthy(false);
result.setMessage("数据库检查异常:" + e.getMessage());
log.error("数据库健康检查异常", e);
}
return result;
}
/**
* Redis健康检查
*/
public HealthCheckResult checkRedis() {
HealthCheckResult result = new HealthCheckResult();
result.setName("redis");
try {
if (redisTemplate == null) {
result.setHealthy(false);
result.setMessage("RedisTemplate未配置");
return result;
}
// 测试Redis连接
String ping = redisTemplate.getConnectionFactory().getConnection().ping();
if ("PONG".equals(ping)) {
result.setHealthy(true);
result.setMessage("Redis连接正常");
} else {
result.setHealthy(false);
result.setMessage("Redis ping响应异常:" + ping);
}
} catch (Exception e) {
result.setHealthy(false);
result.setMessage("Redis连接失败:" + e.getMessage());
log.error("Redis健康检查失败", e);
}
return result;
}
/**
* 自定义健康检查(示例)
*/
public HealthCheckResult checkCustom() {
HealthCheckResult result = new HealthCheckResult();
result.setName("custom");
try {
// 自定义检查逻辑(如:检查第三方接口、文件存储等)
result.setHealthy(true);
result.setMessage("自定义检查通过");
} catch (Exception e) {
result.setHealthy(false);
result.setMessage("自定义检查失败:" + e.getMessage());
log.error("自定义健康检查失败", e);
}
return result;
}
/**
* 快速健康检查(仅检查核心组件)
*/
public boolean quickCheck() {
HealthCheckResult dbResult = checkDatabase();
HealthCheckResult redisResult = checkRedis();
return dbResult.isHealthy() && redisResult.isHealthy();
}
/**
* 健康检查结果DTO
*/
public static class HealthCheckResult {
/**
* 检查项名称
*/
private String name;
/**
* 是否健康
*/
private boolean healthy;
/**
* 检查消息
*/
private String message;
/**
* 检查时间戳(毫秒)
*/
private long timestamp = System.currentTimeMillis();
// Getter & Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isHealthy() {
return healthy;
}
public void setHealthy(boolean healthy) {
this.healthy = healthy;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
}
// 健康检查接口(暴露HTTP端点)
package com.xxx.api.controller;
import com.xxx.common.monitor.util.HealthCheckUtil;
import com.xxx.common.web.response.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 健康检查接口
* 常用度:90%
* 所属层级:控制层
*/
@RestController
@RequestMapping("/actuator/custom/health")
public class HealthCheckController {
@Autowired
private HealthCheckUtil healthCheckUtil;
@GetMapping("/all")
public ApiResponse<Map<String, HealthCheckUtil.HealthCheckResult>> checkAll() {
return ApiResponse.success(healthCheckUtil.checkAll());
}
@GetMapping("/quick")
public ApiResponse<Boolean> quickCheck() {
return ApiResponse.success(healthCheckUtil.quickCheck());
}
@GetMapping("/database")
public ApiResponse<HealthCheckUtil.HealthCheckResult> checkDatabase() {
return ApiResponse.success(healthCheckUtil.checkDatabase());
}
@GetMapping("/redis")
public ApiResponse<HealthCheckUtil.HealthCheckResult> checkRedis() {
return ApiResponse.success(healthCheckUtil.checkRedis());
}
}
2.3 ExceptionLogUtil(异常日志工具)
java
package com.xxx.common.monitor.util;
import com.xxx.common.exception.BusinessException;
import com.xxx.common.exception.ParamInvalidException;
import com.xxx.common.exception.SystemException;
import com.xxx.common.util.TraceIdUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 异常日志工具(统一记录异常,支持告警通知)
* 常用度:90%
* 所属层级:通用层
*
* @author xxx
* @date 2025-11-30
*/
@Slf4j
@Component
public class ExceptionLogUtil {
@Autowired
private LogFormatter logFormatter;
/**
* 记录业务异常
*/
public void recordBusinessException(BusinessException e, HttpServletRequest request) {
Map<String, Object> exceptionInfo = buildExceptionInfo(e, request);
exceptionInfo.put("exceptionType", "BUSINESS");
// 输出日志
log.warn(logFormatter.formatExceptionLog(
TraceIdUtil.getTraceId(),
"BUSINESS_EXCEPTION",
"业务异常",
e
));
// 扩展:业务异常一般不触发告警,可根据错误码配置特殊告警规则
if (e.getErrorCode().getCode() >= 600 && e.getErrorCode().getCode() <= 699) {
sendAlarm(exceptionInfo, "WARN");
}
}
/**
* 记录参数异常
*/
public void recordParamException(ParamInvalidException e, HttpServletRequest request) {
Map<String, Object> exceptionInfo = buildExceptionInfo(e, request);
exceptionInfo.put("exceptionType", "PARAM");
log.warn(logFormatter.formatExceptionLog(
TraceIdUtil.getTraceId(),
"PARAM_EXCEPTION",
"参数异常",
e
));
// 参数异常一般不告警
}
/**
* 记录系统异常
*/
public void recordSystemException(SystemException e, HttpServletRequest request) {
Map<String, Object> exceptionInfo = buildExceptionInfo(e, request);
exceptionInfo.put("exceptionType", "SYSTEM");
log.error(logFormatter.formatExceptionLog(
TraceIdUtil.getTraceId(),
"SYSTEM_EXCEPTION",
"系统异常",
e
));
// 系统异常触发告警
sendAlarm(exceptionInfo, "ERROR");
}
/**
* 记录未知异常
*/
public void recordUnknownException(Exception e, HttpServletRequest request) {
Map<String, Object> exceptionInfo = buildExceptionInfo(e, request);
exceptionInfo.put("exceptionType", "UNKNOWN");
log.error(logFormatter.formatExceptionLog(
TraceIdUtil.getTraceId(),
"UNKNOWN_EXCEPTION",
"未知异常",
e
));
// 未知异常触发告警
sendAlarm(exceptionInfo, "ERROR");
}
/**
* 构建异常信息
*/
private Map<String, Object> buildExceptionInfo(Throwable e, HttpServletRequest request) {
Map<String, Object> exceptionInfo = new HashMap<>();
exceptionInfo.put("traceId", TraceIdUtil.getTraceId());
exceptionInfo.put("timestamp", LocalDateTime.now());
exceptionInfo.put("exceptionClass", e.getClass().getName());
exceptionInfo.put("exceptionMessage", e.getMessage());
if (request != null) {
exceptionInfo.put("requestURI", request.getRequestURI());
exceptionInfo.put("requestMethod", request.getMethod());
exceptionInfo.put("clientIp", getClientIp(request));
exceptionInfo.put("userAgent", request.getHeader("User-Agent"));
}
// 记录异常堆栈(非生产环境)
if (!isProdEnv()) {
StringBuilder stackTrace = new StringBuilder();
for (StackTraceElement element : e.getStackTrace()) {
stackTrace.append(element.toString()).append("\n");
}
exceptionInfo.put("stackTrace", stackTrace.toString());
}
return exceptionInfo;
}
/**
* 发送告警通知(示例:邮件/钉钉/企业微信)
*/
private void sendAlarm(Map<String, Object> exceptionInfo, String level) {
// 此处可实现告警逻辑,如:
// 1. 钉钉机器人推送
// 2. 企业微信机器人推送
// 3. 邮件告警
// 4. 短信告警(紧急)
log.info("发送{}级告警:{}", level, exceptionInfo);
// 示例:DingDingAlarmUtil.send(exceptionInfo, level);
}
/**
* 判断是否为生产环境
*/
private boolean isProdEnv() {
String env = System.getProperty("spring.profiles.active", "dev");
return "prod".equals(env);
}
/**
* 获取客户端IP
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
大厂规范
- 包名规范 :统一使用
com.xxx.模块.子模块格式,如com.xxx.common.config、com.xxx.common.util; - 命名规范:类名使用大驼峰,方法名/变量名使用小驼峰,常量全大写下划线分隔;
- 异常规范:区分业务/系统/参数异常,生产环境隐藏堆栈,异常信息友好;
- 日志规范:全链路TraceId、结构化输出、敏感信息脱敏;
- 安全规范:防XSS/SQL注入、接口签名、幂等、防刷、敏感信息加密/脱敏;
- 性能规范:线程池防OOM、本地缓存减少DB查询、分页限制最大条数。