对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <groupId>com.iailab</groupId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-biz-tenant</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>多租户</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Web 相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-security</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- DB 相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-mybatis</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-redis</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.postgresql</groupId> |
| | | <artifactId>postgresql</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- RPC 远程调用相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-rpc</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <!-- Job 定时任务相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-job</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <!-- 消息队列相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-mq</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.kafka</groupId> |
| | | <artifactId>spring-kafka</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.amqp</groupId> |
| | | <artifactId>spring-rabbit</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.apache.rocketmq</groupId> |
| | | <artifactId>rocketmq-spring-boot-starter</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <!-- Test 测试相关 --> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-test</artifactId> |
| | | <scope>test</scope> |
| | | </dependency> |
| | | |
| | | <!-- 工具类相关 --> |
| | | <dependency> |
| | | <groupId>com.google.guava</groupId> |
| | | <artifactId>guava</artifactId> |
| | | </dependency> |
| | | |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.config; |
| | | |
| | | import com.baomidou.dynamic.datasource.processor.DsProcessor; |
| | | import com.baomidou.dynamic.datasource.processor.DsSpelExpressionProcessor; |
| | | import com.iailab.framework.common.enums.WebFilterOrderEnum; |
| | | import com.iailab.framework.mybatis.core.util.MyBatisUtils; |
| | | import com.iailab.framework.redis.config.IailabCacheProperties; |
| | | import com.iailab.framework.tenant.core.aop.TenantIgnoreAspect; |
| | | import com.iailab.framework.tenant.core.db.TenantDatabaseInterceptor; |
| | | import com.iailab.framework.tenant.core.db.dynamic.TenantDsProcessor; |
| | | import com.iailab.framework.tenant.core.job.TenantJobAspect; |
| | | import com.iailab.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer; |
| | | import com.iailab.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor; |
| | | import com.iailab.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer; |
| | | import com.iailab.framework.tenant.core.redis.TenantRedisCacheManager; |
| | | import com.iailab.framework.tenant.core.security.TenantSecurityWebFilter; |
| | | import com.iailab.framework.tenant.core.service.TenantFrameworkService; |
| | | import com.iailab.framework.tenant.core.service.TenantFrameworkServiceImpl; |
| | | import com.iailab.framework.tenant.core.web.TenantContextWebFilter; |
| | | import com.iailab.framework.web.config.WebProperties; |
| | | import com.iailab.framework.web.core.handler.GlobalExceptionHandler; |
| | | import com.iailab.module.system.api.tenant.TenantApi; |
| | | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
| | | import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.context.annotation.Primary; |
| | | import org.springframework.data.redis.cache.BatchStrategies; |
| | | import org.springframework.data.redis.cache.RedisCacheConfiguration; |
| | | import org.springframework.data.redis.cache.RedisCacheManager; |
| | | import org.springframework.data.redis.cache.RedisCacheWriter; |
| | | import org.springframework.data.redis.connection.RedisConnectionFactory; |
| | | import org.springframework.data.redis.core.RedisTemplate; |
| | | |
| | | import java.util.Objects; |
| | | |
| | | @AutoConfiguration |
| | | @ConditionalOnProperty(prefix = "iailab.tenant", value = "enable", matchIfMissing = true) // 允许使用 iailab.tenant.enable=false 禁用多租户 |
| | | @EnableConfigurationProperties(TenantProperties.class) |
| | | public class IailabTenantAutoConfiguration { |
| | | |
| | | @Bean |
| | | public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) { |
| | | return new TenantFrameworkServiceImpl(tenantApi); |
| | | } |
| | | |
| | | // ========== AOP ========== |
| | | |
| | | @Bean |
| | | public TenantIgnoreAspect tenantIgnoreAspect() { |
| | | return new TenantIgnoreAspect(); |
| | | } |
| | | |
| | | // ========== DB ========== |
| | | |
| | | @Bean |
| | | public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties, |
| | | MybatisPlusInterceptor interceptor) { |
| | | TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties)); |
| | | // 添加到 interceptor 中 |
| | | // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 |
| | | MyBatisUtils.addInterceptor(interceptor, inner, 0); |
| | | return inner; |
| | | } |
| | | |
| | | @Bean |
| | | public DsProcessor dsProcessor( |
| | | // TenantFrameworkService tenantFrameworkService, |
| | | // DataSource dataSource, |
| | | // DefaultDataSourceCreator dataSourceCreator |
| | | ) { |
| | | // TenantDsProcessor tenantDsProcessor = new TenantDsProcessor(tenantFrameworkService, dataSourceCreator); |
| | | TenantDsProcessor tenantDsProcessor = new TenantDsProcessor(); |
| | | tenantDsProcessor.setNextProcessor(new DsSpelExpressionProcessor()); |
| | | return tenantDsProcessor; |
| | | } |
| | | |
| | | // ========== WEB ========== |
| | | |
| | | @Bean |
| | | public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() { |
| | | FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>(); |
| | | registrationBean.setFilter(new TenantContextWebFilter()); |
| | | registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); |
| | | return registrationBean; |
| | | } |
| | | |
| | | // ========== Security ========== |
| | | |
| | | @Bean |
| | | public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties, |
| | | WebProperties webProperties, |
| | | GlobalExceptionHandler globalExceptionHandler, |
| | | TenantFrameworkService tenantFrameworkService) { |
| | | FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>(); |
| | | registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties, |
| | | globalExceptionHandler, tenantFrameworkService)); |
| | | registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); |
| | | return registrationBean; |
| | | } |
| | | |
| | | // ========== Job ========== |
| | | |
| | | @Bean |
| | | @ConditionalOnClass(name = "com.xxl.job.core.handler.annotation.XxlJob") |
| | | public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) { |
| | | return new TenantJobAspect(tenantFrameworkService); |
| | | } |
| | | |
| | | // ========== MQ ========== |
| | | |
| | | /** |
| | | * 多租户 Redis 消息队列的配置类 |
| | | * |
| | | * 为什么要单独一个配置类呢?如果直接把 TenantRedisMessageInterceptor Bean 的初始化放外面,会报 RedisMessageInterceptor 类不存在的错误 |
| | | */ |
| | | @Configuration |
| | | @ConditionalOnClass(name = "com.iailab.framework.mq.redis.core.RedisMQTemplate") |
| | | public static class TenantRedisMQAutoConfiguration { |
| | | |
| | | @Bean |
| | | public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { |
| | | return new TenantRedisMessageInterceptor(); |
| | | } |
| | | |
| | | } |
| | | |
| | | @Bean |
| | | @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") |
| | | public TenantRabbitMQInitializer tenantRabbitMQInitializer() { |
| | | return new TenantRabbitMQInitializer(); |
| | | } |
| | | |
| | | @Bean |
| | | @ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate") |
| | | public TenantRocketMQInitializer tenantRocketMQInitializer() { |
| | | return new TenantRocketMQInitializer(); |
| | | } |
| | | |
| | | // ========== Redis ========== |
| | | |
| | | @Bean |
| | | @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean |
| | | public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate, |
| | | RedisCacheConfiguration redisCacheConfiguration, |
| | | IailabCacheProperties iailabCacheProperties, |
| | | TenantProperties tenantProperties) { |
| | | // 创建 RedisCacheWriter 对象 |
| | | RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); |
| | | RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, |
| | | BatchStrategies.scan(iailabCacheProperties.getRedisScanBatchSize())); |
| | | // 创建 TenantRedisCacheManager 对象 |
| | | return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches()); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.config; |
| | | |
| | | import com.iailab.framework.tenant.core.rpc.TenantRequestInterceptor; |
| | | import com.iailab.module.system.api.tenant.TenantApi; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.cloud.openfeign.EnableFeignClients; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | |
| | | @AutoConfiguration |
| | | @ConditionalOnProperty(prefix = "iailab.tenant", value = "enable", matchIfMissing = true) // 允许使用 iailab.tenant.enable=false 禁用多租户 |
| | | @EnableFeignClients(clients = TenantApi.class) // 主要是引入相关的 API 服务 |
| | | public class IailabTenantRpcAutoConfiguration { |
| | | |
| | | @Bean |
| | | public TenantRequestInterceptor tenantRequestInterceptor() { |
| | | return new TenantRequestInterceptor(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.config; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | |
| | | import java.util.Collections; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * 多租户配置 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @ConfigurationProperties(prefix = "iailab.tenant") |
| | | @Data |
| | | public class TenantProperties { |
| | | |
| | | /** |
| | | * 租户是否开启 |
| | | */ |
| | | private static final Boolean ENABLE_DEFAULT = true; |
| | | |
| | | /** |
| | | * 是否开启 |
| | | */ |
| | | private Boolean enable = ENABLE_DEFAULT; |
| | | |
| | | /** |
| | | * 需要忽略多租户的请求 |
| | | * |
| | | * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API! |
| | | */ |
| | | private Set<String> ignoreUrls = Collections.emptySet(); |
| | | |
| | | /** |
| | | * 需要忽略多租户的表 |
| | | * |
| | | * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 |
| | | */ |
| | | private Set<String> ignoreTables = Collections.emptySet(); |
| | | |
| | | /** |
| | | * 需要忽略多租户的 Spring Cache 缓存 |
| | | * |
| | | * 即默认所有缓存都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 |
| | | */ |
| | | private Set<String> ignoreCaches = Collections.emptySet(); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.aop; |
| | | |
| | | import java.lang.annotation.*; |
| | | |
| | | /** |
| | | * 忽略租户,标记指定方法不进行租户的自动过滤 |
| | | * |
| | | * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤: |
| | | * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的 |
| | | * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Target({ElementType.METHOD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Inherited |
| | | public @interface TenantIgnore { |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.aop; |
| | | |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import com.iailab.framework.tenant.core.util.TenantUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.aspectj.lang.ProceedingJoinPoint; |
| | | import org.aspectj.lang.annotation.Around; |
| | | import org.aspectj.lang.annotation.Aspect; |
| | | |
| | | /** |
| | | * 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。 |
| | | * 例如说,一个定时任务,读取所有数据,进行处理。 |
| | | * 又例如说,读取所有数据,进行缓存。 |
| | | * |
| | | * 整体逻辑的实现,和 {@link TenantUtils#executeIgnore(Runnable)} 需要保持一致 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Aspect |
| | | @Slf4j |
| | | public class TenantIgnoreAspect { |
| | | |
| | | @Around("@annotation(tenantIgnore)") |
| | | public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable { |
| | | Boolean oldIgnore = TenantContextHolder.isIgnore(); |
| | | try { |
| | | TenantContextHolder.setIgnore(true); |
| | | // 执行逻辑 |
| | | return joinPoint.proceed(); |
| | | } finally { |
| | | TenantContextHolder.setIgnore(oldIgnore); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.context; |
| | | |
| | | import com.alibaba.ttl.TransmittableThreadLocal; |
| | | import com.iailab.framework.common.enums.DocumentEnum; |
| | | |
| | | /** |
| | | * 数据源上下文 Holder |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DataContextHolder { |
| | | |
| | | /** |
| | | * 数据源id |
| | | */ |
| | | private static final ThreadLocal<Long> DATA_SOURCE_ID = new TransmittableThreadLocal<>(); |
| | | |
| | | /** |
| | | * 数据源id |
| | | * |
| | | * @return 租户编号 |
| | | */ |
| | | public static Long getDataSourceId() { |
| | | return DATA_SOURCE_ID.get(); |
| | | } |
| | | |
| | | /** |
| | | * 数据源id。如果不存在,则抛出 NullPointerException 异常 |
| | | * |
| | | * @return 租户编号 |
| | | */ |
| | | public static Long getRequiredDataSourceId() { |
| | | Long dataSourceId = getDataSourceId(); |
| | | if (dataSourceId == null) { |
| | | throw new NullPointerException("DataContextHolder 不存在数据源id!可参考文档:" |
| | | + DocumentEnum.TENANT.getUrl()); |
| | | } |
| | | return dataSourceId; |
| | | } |
| | | |
| | | public static void setDataSourceId(Long dataSourceId) { |
| | | DATA_SOURCE_ID.set(dataSourceId); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.context; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.enums.DocumentEnum; |
| | | import com.alibaba.ttl.TransmittableThreadLocal; |
| | | |
| | | /** |
| | | * 多租户上下文 Holder |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantContextHolder { |
| | | |
| | | /** |
| | | * 当前租户编号 |
| | | */ |
| | | private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>(); |
| | | |
| | | /** |
| | | * 是否忽略租户 |
| | | */ |
| | | private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>(); |
| | | |
| | | /** |
| | | * 获得租户编号 |
| | | * |
| | | * @return 租户编号 |
| | | */ |
| | | public static Long getTenantId() { |
| | | return TENANT_ID.get(); |
| | | } |
| | | |
| | | /** |
| | | * 获得租户编号。如果不存在,则抛出 NullPointerException 异常 |
| | | * |
| | | * @return 租户编号 |
| | | */ |
| | | public static Long getRequiredTenantId() { |
| | | Long tenantId = getTenantId(); |
| | | if (tenantId == null) { |
| | | throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:" |
| | | + DocumentEnum.TENANT.getUrl()); |
| | | } |
| | | return tenantId; |
| | | } |
| | | |
| | | public static void setTenantId(Long tenantId) { |
| | | TENANT_ID.set(tenantId); |
| | | } |
| | | |
| | | public static void setIgnore(Boolean ignore) { |
| | | IGNORE.set(ignore); |
| | | } |
| | | |
| | | /** |
| | | * 当前是否忽略租户 |
| | | * |
| | | * @return 是否忽略 |
| | | */ |
| | | public static boolean isIgnore() { |
| | | return Boolean.TRUE.equals(IGNORE.get()); |
| | | } |
| | | |
| | | public static void clear() { |
| | | TENANT_ID.remove(); |
| | | IGNORE.remove(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.db; |
| | | |
| | | import com.iailab.framework.mybatis.core.dataobject.BaseDO; |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | |
| | | /** |
| | | * 拓展多租户的 BaseDO 基类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | public abstract class TenantBaseDO extends BaseDO { |
| | | |
| | | /** |
| | | * 多租户编号 |
| | | */ |
| | | private Long tenantId; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.db; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.iailab.framework.tenant.config.TenantProperties; |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; |
| | | import net.sf.jsqlparser.expression.Expression; |
| | | import net.sf.jsqlparser.expression.LongValue; |
| | | |
| | | import java.util.HashSet; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantDatabaseInterceptor implements TenantLineHandler { |
| | | |
| | | private final Set<String> ignoreTables = new HashSet<>(); |
| | | |
| | | public TenantDatabaseInterceptor(TenantProperties properties) { |
| | | // 不同 DB 下,大小写的习惯不同,所以需要都添加进去 |
| | | properties.getIgnoreTables().forEach(table -> { |
| | | ignoreTables.add(table.toLowerCase()); |
| | | ignoreTables.add(table.toUpperCase()); |
| | | }); |
| | | // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错 |
| | | ignoreTables.add("DUAL"); |
| | | } |
| | | |
| | | @Override |
| | | public Expression getTenantId() { |
| | | return new LongValue(TenantContextHolder.getRequiredTenantId()); |
| | | } |
| | | |
| | | @Override |
| | | public boolean ignoreTable(String tableName) { |
| | | return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户 |
| | | || CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表 |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.db.dynamic; |
| | | |
| | | import com.baomidou.dynamic.datasource.annotation.DS; |
| | | |
| | | import java.lang.annotation.*; |
| | | |
| | | /** |
| | | * 使用数据源所在的数据源 |
| | | * |
| | | * 使用方式:当我们希望一个表使用租户所在的数据源,可以在该表的 Mapper 上添加该注解 |
| | | * |
| | | * @author 芋道源码 |
| | | */ |
| | | @Target({ElementType.TYPE, ElementType.METHOD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Documented |
| | | @DS(DataDS.KEY) |
| | | public @interface DataDS { |
| | | |
| | | /** |
| | | * 数据源的占位符 |
| | | */ |
| | | String KEY = "#context.dataSourceId"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.db.dynamic; |
| | | |
| | | import com.baomidou.dynamic.datasource.annotation.DS; |
| | | |
| | | import java.lang.annotation.*; |
| | | |
| | | /** |
| | | * 使用租户所在的数据源 |
| | | * |
| | | * 使用方式:当我们希望一个表使用租户所在的数据源,可以在该表的 Mapper 上添加该注解 |
| | | * |
| | | * @author 芋道源码 |
| | | */ |
| | | @Target({ElementType.TYPE, ElementType.METHOD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Documented |
| | | @DS(TenantDS.KEY) |
| | | public @interface TenantDS { |
| | | |
| | | /** |
| | | * 租户对应的数据源的占位符 |
| | | */ |
| | | String KEY = "#context.tenantId"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.db.dynamic; |
| | | |
| | | import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; |
| | | import com.baomidou.dynamic.datasource.creator.DataSourceProperty; |
| | | import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator; |
| | | import com.baomidou.dynamic.datasource.processor.DsProcessor; |
| | | import com.iailab.framework.tenant.core.context.DataContextHolder; |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import com.iailab.framework.tenant.core.service.TenantFrameworkService; |
| | | import com.iailab.module.infra.api.db.DataSourceConfigServiceApi; |
| | | import com.iailab.module.infra.api.db.dto.DataSourceConfigRespDTO; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.aopalliance.intercept.MethodInvocation; |
| | | import org.springframework.context.annotation.Lazy; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.sql.DataSource; |
| | | import java.util.Objects; |
| | | |
| | | /** |
| | | * 基于 {@link TenantDS} 的数据源处理器 |
| | | * |
| | | * 1. 如果有 @TenantDS 注解,返回该租户的数据源 |
| | | * 2. 如果该租户的数据源未创建,则进行创建 |
| | | * |
| | | * @author 芋道源码 |
| | | */ |
| | | @RequiredArgsConstructor |
| | | public class TenantDsProcessor extends DsProcessor { |
| | | |
| | | /** |
| | | * 用于获取租户数据源配置的 Service |
| | | */ |
| | | @Resource |
| | | @Lazy |
| | | private TenantFrameworkService tenantFrameworkService; |
| | | |
| | | /** |
| | | * 动态数据源 |
| | | */ |
| | | @Resource |
| | | @Lazy // 为什么添加 @Lazy 注解?因为它和 DynamicRoutingDataSource 相互依赖,导致无法初始化 |
| | | private DynamicRoutingDataSource dynamicRoutingDataSource; |
| | | |
| | | /** |
| | | * 用于创建租户数据源的 Creator |
| | | */ |
| | | @Resource |
| | | @Lazy |
| | | private DefaultDataSourceCreator dataSourceCreator; |
| | | |
| | | @Resource |
| | | @Lazy |
| | | private DataSourceConfigServiceApi dataSourceConfigServiceApi; |
| | | |
| | | @Override |
| | | public boolean matches(String key) { |
| | | return Objects.equals(key, TenantDS.KEY) || Objects.equals(key, DataDS.KEY); |
| | | } |
| | | |
| | | @Override |
| | | public String doDetermineDatasource(MethodInvocation invocation, String key) { |
| | | if (DataDS.KEY.equals(key)){ |
| | | // 获得数据源配置 |
| | | Long dataSourceId = DataContextHolder.getRequiredDataSourceId(); |
| | | DataSourceConfigRespDTO dataSourceConfigRespDTO = dataSourceConfigServiceApi.getDataSourceConfig(dataSourceId); |
| | | DataSourceProperty dataSourceProperty = new DataSourceProperty(); |
| | | dataSourceProperty.setPoolName(dataSourceConfigRespDTO.getName()); |
| | | dataSourceProperty.setUrl(dataSourceConfigRespDTO.getUrl()); |
| | | dataSourceProperty.setUsername(dataSourceConfigRespDTO.getUsername()); |
| | | dataSourceProperty.setPassword(dataSourceConfigRespDTO.getPassword()); |
| | | // 创建 or 创建数据源,并返回数据源名字 |
| | | return createDatasourceIfAbsent(dataSourceProperty); |
| | | }else if(TenantDS.KEY.equals(key)){ |
| | | // 获得数据源配置 |
| | | Long tenantId = TenantContextHolder.getRequiredTenantId(); |
| | | DataSourceProperty dataSourceProperty = tenantFrameworkService.getDataSourceProperty(tenantId); |
| | | // 创建 or 创建数据源,并返回数据源名字 |
| | | return createDatasourceIfAbsent(dataSourceProperty); |
| | | } |
| | | return key; |
| | | } |
| | | |
| | | private String createDatasourceIfAbsent(DataSourceProperty dataSourceProperty) { |
| | | // 1. 重点:如果数据源不存在,则进行创建 |
| | | if (isDataSourceNotExist(dataSourceProperty)) { |
| | | // 问题一:为什么要加锁?因为,如果多个线程同时执行到这里,会导致多次创建数据源 |
| | | // 问题二:为什么要使用 poolName 加锁?保证多个不同的 poolName 可以并发创建数据源 |
| | | // 问题三:为什么要使用 intern 方法?因为,intern 方法,会返回一个字符串的常量池中的引用 |
| | | // intern 的说明,可见 https://www.cnblogs.com/xrq730/p/6662232.html 文章 |
| | | synchronized (dataSourceProperty.getPoolName().intern()) { |
| | | if (isDataSourceNotExist(dataSourceProperty)) { |
| | | DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty); |
| | | dynamicRoutingDataSource.addDataSource(dataSourceProperty.getPoolName(), dataSource); |
| | | } |
| | | } |
| | | } |
| | | // 2. 返回数据源的名字 |
| | | return dataSourceProperty.getPoolName(); |
| | | } |
| | | |
| | | private boolean isDataSourceNotExist(DataSourceProperty dataSourceProperty) { |
| | | return !dynamicRoutingDataSource.getDataSources().containsKey(dataSourceProperty.getPoolName()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.job; |
| | | |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 多租户 Job 注解 |
| | | */ |
| | | @Target({ElementType.METHOD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | public @interface TenantJob { |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.job; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.exceptions.ExceptionUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.tenant.core.service.TenantFrameworkService; |
| | | import com.iailab.framework.tenant.core.util.TenantUtils; |
| | | import com.xxl.job.core.context.XxlJobHelper; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.lang3.exception.ExceptionUtils; |
| | | import org.aspectj.lang.ProceedingJoinPoint; |
| | | import org.aspectj.lang.annotation.Around; |
| | | import org.aspectj.lang.annotation.Aspect; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | /** |
| | | * 多租户 JobHandler AOP |
| | | * 任务执行时,会按照租户逐个执行 Job 的逻辑 |
| | | * |
| | | * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Aspect |
| | | @RequiredArgsConstructor |
| | | @Slf4j |
| | | public class TenantJobAspect { |
| | | |
| | | private final TenantFrameworkService tenantFrameworkService; |
| | | |
| | | @Around("@annotation(tenantJob)") |
| | | public void around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) { |
| | | // 获得租户列表 |
| | | List<Long> tenantIds = tenantFrameworkService.getTenantIds(); |
| | | if (CollUtil.isEmpty(tenantIds)) { |
| | | return; |
| | | } |
| | | |
| | | // 逐个租户,执行 Job |
| | | Map<Long, String> results = new ConcurrentHashMap<>(); |
| | | tenantIds.parallelStream().forEach(tenantId -> { |
| | | // TODO iailab:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况 |
| | | TenantUtils.execute(tenantId, () -> { |
| | | try { |
| | | System.out.println("租户id:" + tenantId); |
| | | joinPoint.proceed(); |
| | | } catch (Throwable e) { |
| | | results.put(tenantId, ExceptionUtil.getRootCauseMessage(e)); |
| | | // 打印异常 |
| | | XxlJobHelper.log(StrUtil.format("[多租户({}) 执行任务({}),发生异常:{}]", |
| | | tenantId, joinPoint.getSignature(), ExceptionUtils.getStackTrace(e))); |
| | | } |
| | | }); |
| | | }); |
| | | // 如果 results 非空,说明发生了异常,标记 XXL-Job 执行失败 |
| | | if (CollUtil.isNotEmpty(results)) { |
| | | XxlJobHelper.handleFail(JsonUtils.toJsonString(results)); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.mq.kafka; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.boot.SpringApplication; |
| | | import org.springframework.boot.env.EnvironmentPostProcessor; |
| | | import org.springframework.core.env.ConfigurableEnvironment; |
| | | |
| | | /** |
| | | * 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类 |
| | | * |
| | | * Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor { |
| | | |
| | | private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes"; |
| | | |
| | | @Override |
| | | public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { |
| | | // 添加 TenantKafkaProducerInterceptor 拦截器 |
| | | try { |
| | | String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES); |
| | | if (StrUtil.isEmpty(value)) { |
| | | value = TenantKafkaProducerInterceptor.class.getName(); |
| | | } else { |
| | | value += "," + TenantKafkaProducerInterceptor.class.getName(); |
| | | } |
| | | environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value); |
| | | } catch (NoClassDefFoundError ignore) { |
| | | // 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖 |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.mq.kafka; |
| | | |
| | | import cn.hutool.core.util.ReflectUtil; |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import org.apache.kafka.clients.producer.ProducerInterceptor; |
| | | import org.apache.kafka.clients.producer.ProducerRecord; |
| | | import org.apache.kafka.clients.producer.RecordMetadata; |
| | | import org.apache.kafka.common.header.Headers; |
| | | import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; |
| | | |
| | | import java.util.Map; |
| | | |
| | | import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
| | | |
| | | /** |
| | | * Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类 |
| | | * |
| | | * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
| | | * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantKafkaProducerInterceptor implements ProducerInterceptor<Object, Object> { |
| | | |
| | | @Override |
| | | public ProducerRecord<Object, Object> onSend(ProducerRecord<Object, Object> record) { |
| | | Long tenantId = TenantContextHolder.getTenantId(); |
| | | if (tenantId != null) { |
| | | Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射 |
| | | headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes()); |
| | | } |
| | | return record; |
| | | } |
| | | |
| | | @Override |
| | | public void onAcknowledgement(RecordMetadata metadata, Exception exception) { |
| | | } |
| | | |
| | | @Override |
| | | public void close() { |
| | | } |
| | | |
| | | @Override |
| | | public void configure(Map<String, ?> configs) { |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.mq.rabbitmq; |
| | | |
| | | import org.springframework.amqp.rabbit.core.RabbitTemplate; |
| | | import org.springframework.beans.BeansException; |
| | | import org.springframework.beans.factory.config.BeanPostProcessor; |
| | | |
| | | /** |
| | | * 多租户的 RabbitMQ 初始化器 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantRabbitMQInitializer implements BeanPostProcessor { |
| | | |
| | | @Override |
| | | public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { |
| | | if (bean instanceof RabbitTemplate) { |
| | | RabbitTemplate rabbitTemplate = (RabbitTemplate) bean; |
| | | rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor()); |
| | | } |
| | | return bean; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.mq.rabbitmq; |
| | | |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import org.apache.kafka.clients.producer.ProducerInterceptor; |
| | | import org.springframework.amqp.AmqpException; |
| | | import org.springframework.amqp.core.Message; |
| | | import org.springframework.amqp.core.MessagePostProcessor; |
| | | import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; |
| | | |
| | | import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
| | | |
| | | /** |
| | | * RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类 |
| | | * |
| | | * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
| | | * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor { |
| | | |
| | | @Override |
| | | public Message postProcessMessage(Message message) throws AmqpException { |
| | | Long tenantId = TenantContextHolder.getTenantId(); |
| | | if (tenantId != null) { |
| | | message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId); |
| | | } |
| | | return message; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.mq.redis; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor; |
| | | import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage; |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | |
| | | import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
| | | |
| | | /** |
| | | * 多租户 {@link AbstractRedisMessage} 拦截器 |
| | | * |
| | | * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
| | | * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantRedisMessageInterceptor implements RedisMessageInterceptor { |
| | | |
| | | @Override |
| | | public void sendMessageBefore(AbstractRedisMessage message) { |
| | | Long tenantId = TenantContextHolder.getTenantId(); |
| | | if (tenantId != null) { |
| | | message.addHeader(HEADER_TENANT_ID, tenantId.toString()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void consumeMessageBefore(AbstractRedisMessage message) { |
| | | String tenantIdStr = message.getHeader(HEADER_TENANT_ID); |
| | | if (StrUtil.isNotEmpty(tenantIdStr)) { |
| | | TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr)); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void consumeMessageAfter(AbstractRedisMessage message) { |
| | | // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况 |
| | | TenantContextHolder.clear(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.mq.rocketmq; |
| | | |
| | | import cn.hutool.core.lang.Assert; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import org.apache.rocketmq.client.hook.ConsumeMessageContext; |
| | | import org.apache.rocketmq.client.hook.ConsumeMessageHook; |
| | | import org.apache.rocketmq.common.message.MessageExt; |
| | | import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; |
| | | |
| | | import java.util.List; |
| | | |
| | | import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
| | | |
| | | /** |
| | | * RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类 |
| | | * |
| | | * Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook { |
| | | |
| | | @Override |
| | | public String hookName() { |
| | | return getClass().getSimpleName(); |
| | | } |
| | | |
| | | @Override |
| | | public void consumeMessageBefore(ConsumeMessageContext context) { |
| | | // 校验,消息必须是单条,不然设置租户可能不正确 |
| | | List<MessageExt> messages = context.getMsgList(); |
| | | Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size()); |
| | | // 设置租户编号 |
| | | String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID); |
| | | if (StrUtil.isNotEmpty(tenantId)) { |
| | | TenantContextHolder.setTenantId(Long.parseLong(tenantId)); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void consumeMessageAfter(ConsumeMessageContext context) { |
| | | TenantContextHolder.clear(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.mq.rocketmq; |
| | | |
| | | import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; |
| | | import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; |
| | | import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; |
| | | import org.apache.rocketmq.client.producer.DefaultMQProducer; |
| | | import org.apache.rocketmq.spring.core.RocketMQTemplate; |
| | | import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer; |
| | | import org.springframework.beans.BeansException; |
| | | import org.springframework.beans.factory.config.BeanPostProcessor; |
| | | |
| | | /** |
| | | * 多租户的 RocketMQ 初始化器 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantRocketMQInitializer implements BeanPostProcessor { |
| | | |
| | | @Override |
| | | public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { |
| | | if (bean instanceof DefaultRocketMQListenerContainer) { |
| | | DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean; |
| | | initTenantConsumer(container.getConsumer()); |
| | | } else if (bean instanceof RocketMQTemplate) { |
| | | RocketMQTemplate template = (RocketMQTemplate) bean; |
| | | initTenantProducer(template.getProducer()); |
| | | } |
| | | return bean; |
| | | } |
| | | |
| | | private void initTenantProducer(DefaultMQProducer producer) { |
| | | if (producer == null) { |
| | | return; |
| | | } |
| | | DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl(); |
| | | if (producerImpl == null) { |
| | | return; |
| | | } |
| | | producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook()); |
| | | } |
| | | |
| | | private void initTenantConsumer(DefaultMQPushConsumer consumer) { |
| | | if (consumer == null) { |
| | | return; |
| | | } |
| | | DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl(); |
| | | if (consumerImpl == null) { |
| | | return; |
| | | } |
| | | consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.mq.rocketmq; |
| | | |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import org.apache.rocketmq.client.hook.SendMessageContext; |
| | | import org.apache.rocketmq.client.hook.SendMessageHook; |
| | | |
| | | import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
| | | |
| | | /** |
| | | * RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类 |
| | | * |
| | | * Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantRocketMQSendMessageHook implements SendMessageHook { |
| | | |
| | | @Override |
| | | public String hookName() { |
| | | return getClass().getSimpleName(); |
| | | } |
| | | |
| | | @Override |
| | | public void sendMessageBefore(SendMessageContext sendMessageContext) { |
| | | Long tenantId = TenantContextHolder.getTenantId(); |
| | | if (tenantId == null) { |
| | | return; |
| | | } |
| | | sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString()); |
| | | } |
| | | |
| | | @Override |
| | | public void sendMessageAfter(SendMessageContext sendMessageContext) { |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.redis; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.iailab.framework.redis.core.TimeoutRedisCacheManager; |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.cache.Cache; |
| | | import org.springframework.data.redis.cache.RedisCacheConfiguration; |
| | | import org.springframework.data.redis.cache.RedisCacheManager; |
| | | import org.springframework.data.redis.cache.RedisCacheWriter; |
| | | |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * 多租户的 {@link RedisCacheManager} 实现类 |
| | | * |
| | | * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀 |
| | | * |
| | | * @author airhead |
| | | */ |
| | | @Slf4j |
| | | public class TenantRedisCacheManager extends TimeoutRedisCacheManager { |
| | | |
| | | private final Set<String> ignoreCaches; |
| | | |
| | | public TenantRedisCacheManager(RedisCacheWriter cacheWriter, |
| | | RedisCacheConfiguration defaultCacheConfiguration, |
| | | Set<String> ignoreCaches) { |
| | | super(cacheWriter, defaultCacheConfiguration); |
| | | this.ignoreCaches = ignoreCaches; |
| | | } |
| | | |
| | | @Override |
| | | public Cache getCache(String name) { |
| | | // 如果开启多租户,则 name 拼接租户后缀 |
| | | if (!TenantContextHolder.isIgnore() |
| | | && TenantContextHolder.getTenantId() != null |
| | | && !CollUtil.contains(ignoreCaches, name)) { |
| | | name = name + ":" + TenantContextHolder.getTenantId(); |
| | | } |
| | | |
| | | // 继续基于父方法 |
| | | return super.getCache(name); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.rpc; |
| | | |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import feign.RequestInterceptor; |
| | | import feign.RequestTemplate; |
| | | |
| | | import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
| | | |
| | | /** |
| | | * Tenant 的 RequestInterceptor 实现类:Feign 请求时,将 {@link TenantContextHolder} 设置到 header 中,继续透传给被调用的服务 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantRequestInterceptor implements RequestInterceptor { |
| | | |
| | | @Override |
| | | public void apply(RequestTemplate requestTemplate) { |
| | | Long tenantId = TenantContextHolder.getTenantId(); |
| | | if (tenantId != null) { |
| | | requestTemplate.header(HEADER_TENANT_ID, String.valueOf(tenantId)); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.security; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import com.iailab.framework.common.pojo.CommonResult; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import com.iailab.framework.security.core.LoginUser; |
| | | import com.iailab.framework.security.core.util.SecurityFrameworkUtils; |
| | | import com.iailab.framework.tenant.config.TenantProperties; |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import com.iailab.framework.tenant.core.service.TenantFrameworkService; |
| | | import com.iailab.framework.web.config.WebProperties; |
| | | import com.iailab.framework.web.core.filter.ApiRequestFilter; |
| | | import com.iailab.framework.web.core.handler.GlobalExceptionHandler; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.util.AntPathMatcher; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | import java.util.Objects; |
| | | |
| | | /** |
| | | * 多租户 Security Web 过滤器 |
| | | * 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。 |
| | | * 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。 |
| | | * 3. 校验租户是合法,例如说被禁用、到期 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class TenantSecurityWebFilter extends ApiRequestFilter { |
| | | |
| | | private final TenantProperties tenantProperties; |
| | | |
| | | private final AntPathMatcher pathMatcher; |
| | | |
| | | private final GlobalExceptionHandler globalExceptionHandler; |
| | | private final TenantFrameworkService tenantFrameworkService; |
| | | |
| | | public TenantSecurityWebFilter(TenantProperties tenantProperties, |
| | | WebProperties webProperties, |
| | | GlobalExceptionHandler globalExceptionHandler, |
| | | TenantFrameworkService tenantFrameworkService) { |
| | | super(webProperties); |
| | | this.tenantProperties = tenantProperties; |
| | | this.pathMatcher = new AntPathMatcher(); |
| | | this.globalExceptionHandler = globalExceptionHandler; |
| | | this.tenantFrameworkService = tenantFrameworkService; |
| | | } |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
| | | throws ServletException, IOException { |
| | | Long tenantId = TenantContextHolder.getTenantId(); |
| | | // 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。 |
| | | LoginUser user = SecurityFrameworkUtils.getLoginUser(); |
| | | if (user != null) { |
| | | // 如果获取不到租户编号,则尝试使用登陆用户的租户编号 |
| | | if (tenantId == null) { |
| | | tenantId = user.getTenantId(); |
| | | TenantContextHolder.setTenantId(tenantId); |
| | | // 如果传递了租户编号,则进行比对租户编号,避免越权问题 |
| | | } else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) { |
| | | log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]", |
| | | user.getTenantId(), user.getId(), user.getUserType(), |
| | | TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod()); |
| | | ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(), |
| | | "您无权访问该租户的数据")); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // 如果非允许忽略租户的 URL,则校验租户是否合法 |
| | | if (!isIgnoreUrl(request)) { |
| | | // 2. 如果请求未带租户的编号,不允许访问。 |
| | | if (tenantId == null) { |
| | | log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod()); |
| | | ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), |
| | | "请求的租户标识未传递,请进行排查")); |
| | | return; |
| | | } |
| | | // 3. 校验租户是合法,例如说被禁用、到期 |
| | | try { |
| | | tenantFrameworkService.validTenant(tenantId); |
| | | } catch (Throwable ex) { |
| | | CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex); |
| | | ServletUtils.writeJSON(response, result); |
| | | return; |
| | | } |
| | | } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错 |
| | | if (tenantId == null) { |
| | | TenantContextHolder.setIgnore(true); |
| | | } |
| | | } |
| | | |
| | | // 继续过滤 |
| | | chain.doFilter(request, response); |
| | | } |
| | | |
| | | private boolean isIgnoreUrl(HttpServletRequest request) { |
| | | // 快速匹配,保证性能 |
| | | if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) { |
| | | return true; |
| | | } |
| | | // 逐个 Ant 路径匹配 |
| | | for (String url : tenantProperties.getIgnoreUrls()) { |
| | | if (pathMatcher.match(url, request.getRequestURI())) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.service; |
| | | |
| | | import com.baomidou.dynamic.datasource.creator.DataSourceProperty; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * Tenant 框架 Service 接口,定义获取租户信息 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface TenantFrameworkService { |
| | | |
| | | /** |
| | | * 获得所有租户 |
| | | * |
| | | * @return 租户编号数组 |
| | | */ |
| | | List<Long> getTenantIds(); |
| | | |
| | | /** |
| | | * 校验租户是否合法 |
| | | * |
| | | * @param id 租户编号 |
| | | */ |
| | | void validTenant(Long id); |
| | | |
| | | /** |
| | | * 获得租户对应的数据源配置 |
| | | * |
| | | * @param id 租户编号 |
| | | * @return 数据源配置 |
| | | */ |
| | | DataSourceProperty getDataSourceProperty(Long id); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.service; |
| | | |
| | | import com.baomidou.dynamic.datasource.creator.DataSourceProperty; |
| | | import com.iailab.framework.common.pojo.CommonResult; |
| | | import com.iailab.framework.common.util.cache.CacheUtils; |
| | | import com.iailab.module.system.api.tenant.TenantApi; |
| | | import com.google.common.cache.CacheLoader; |
| | | import com.google.common.cache.LoadingCache; |
| | | |
| | | import com.iailab.module.system.api.tenant.dto.TenantDataSourceConfigRespDTO; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.SneakyThrows; |
| | | |
| | | import java.time.Duration; |
| | | import java.util.List; |
| | | |
| | | import static com.iailab.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; |
| | | |
| | | /** |
| | | * Tenant 框架 Service 实现类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | public class TenantFrameworkServiceImpl implements TenantFrameworkService { |
| | | |
| | | private final TenantApi tenantApi; |
| | | |
| | | /** |
| | | * 针对 {@link #getTenantIds()} 的缓存 |
| | | */ |
| | | private final LoadingCache<Object, List<Long>> getTenantIdsCache = buildAsyncReloadingCache( |
| | | Duration.ofMinutes(1L), // 过期时间 1 分钟 |
| | | new CacheLoader<Object, List<Long>>() { |
| | | |
| | | @Override |
| | | public List<Long> load(Object key) { |
| | | return tenantApi.getTenantIdList().getCheckedData(); |
| | | } |
| | | |
| | | }); |
| | | |
| | | /** |
| | | * 针对 {@link #validTenant(Long)} 的缓存 |
| | | */ |
| | | private final LoadingCache<Long, CommonResult<Boolean>> validTenantCache = buildAsyncReloadingCache( |
| | | Duration.ofMinutes(1L), // 过期时间 1 分钟 |
| | | new CacheLoader<Long, CommonResult<Boolean>>() { |
| | | |
| | | @Override |
| | | public CommonResult<Boolean> load(Long id) { |
| | | return tenantApi.validTenant(id); |
| | | } |
| | | |
| | | }); |
| | | |
| | | /** |
| | | * 针对 {@link #getDataSourceProperty(Long)} 的缓存 |
| | | */ |
| | | private final LoadingCache<Long, DataSourceProperty> dataSourcePropertyCache = CacheUtils.buildAsyncReloadingCache( |
| | | Duration.ofMinutes(1L), // 过期时间 1 分钟 |
| | | new CacheLoader<Long, DataSourceProperty>() { |
| | | |
| | | @Override |
| | | public DataSourceProperty load(Long id) { |
| | | // 获得租户对应的数据源配置 |
| | | TenantDataSourceConfigRespDTO dataSourceConfig = tenantApi.getTenantDataSourceConfig(id); |
| | | if (dataSourceConfig == null) { |
| | | return null; |
| | | } |
| | | // 转换成 dynamic-datasource 配置 |
| | | // return new DataSourceProperty() |
| | | // .setPoolName(dataSourceConfig.getName()).setUrl(dataSourceConfig.getUrl()) |
| | | // .setUsername(dataSourceConfig.getUsername()).setPassword(dataSourceConfig.getPassword()); |
| | | |
| | | DataSourceProperty ds = new DataSourceProperty(); |
| | | ds.setPoolName(dataSourceConfig.getName()); |
| | | ds.setUrl(dataSourceConfig.getUrl()); |
| | | ds.setUsername(dataSourceConfig.getUsername()); |
| | | ds.setPassword(dataSourceConfig.getPassword()); |
| | | return ds; |
| | | } |
| | | |
| | | }); |
| | | |
| | | @Override |
| | | @SneakyThrows |
| | | public List<Long> getTenantIds() { |
| | | return getTenantIdsCache.get(Boolean.TRUE); |
| | | } |
| | | |
| | | @Override |
| | | @SneakyThrows |
| | | public void validTenant(Long id) { |
| | | validTenantCache.get(id).checkError(); |
| | | } |
| | | |
| | | @Override |
| | | @SneakyThrows |
| | | public DataSourceProperty getDataSourceProperty(Long id) { |
| | | return dataSourcePropertyCache.get(id); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.util; |
| | | |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | |
| | | import java.util.Map; |
| | | import java.util.concurrent.Callable; |
| | | |
| | | import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
| | | |
| | | /** |
| | | * 多租户 Util |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantUtils { |
| | | |
| | | /** |
| | | * 使用指定租户,执行对应的逻辑 |
| | | * |
| | | * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 |
| | | * 当然,执行完成后,还是会恢复回去 |
| | | * |
| | | * @param tenantId 租户编号 |
| | | * @param runnable 逻辑 |
| | | */ |
| | | public static void execute(Long tenantId, Runnable runnable) { |
| | | Long oldTenantId = TenantContextHolder.getTenantId(); |
| | | Boolean oldIgnore = TenantContextHolder.isIgnore(); |
| | | try { |
| | | TenantContextHolder.setTenantId(tenantId); |
| | | TenantContextHolder.setIgnore(false); |
| | | // 执行逻辑 |
| | | runnable.run(); |
| | | } finally { |
| | | TenantContextHolder.setTenantId(oldTenantId); |
| | | TenantContextHolder.setIgnore(oldIgnore); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 使用指定租户,执行对应的逻辑 |
| | | * |
| | | * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 |
| | | * 当然,执行完成后,还是会恢复回去 |
| | | * |
| | | * @param tenantId 租户编号 |
| | | * @param callable 逻辑 |
| | | */ |
| | | public static <V> V execute(Long tenantId, Callable<V> callable) { |
| | | Long oldTenantId = TenantContextHolder.getTenantId(); |
| | | Boolean oldIgnore = TenantContextHolder.isIgnore(); |
| | | try { |
| | | TenantContextHolder.setTenantId(tenantId); |
| | | TenantContextHolder.setIgnore(false); |
| | | // 执行逻辑 |
| | | return callable.call(); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException(e); |
| | | } finally { |
| | | TenantContextHolder.setTenantId(oldTenantId); |
| | | TenantContextHolder.setIgnore(oldIgnore); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 忽略租户,执行对应的逻辑 |
| | | * |
| | | * @param runnable 逻辑 |
| | | */ |
| | | public static void executeIgnore(Runnable runnable) { |
| | | Boolean oldIgnore = TenantContextHolder.isIgnore(); |
| | | try { |
| | | TenantContextHolder.setIgnore(true); |
| | | // 执行逻辑 |
| | | runnable.run(); |
| | | } finally { |
| | | TenantContextHolder.setIgnore(oldIgnore); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 将多租户编号,添加到 header 中 |
| | | * |
| | | * @param headers HTTP 请求 headers |
| | | * @param tenantId 租户编号 |
| | | */ |
| | | public static void addTenantHeader(Map<String, String> headers, Long tenantId) { |
| | | if (tenantId != null) { |
| | | headers.put(HEADER_TENANT_ID, tenantId.toString()); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tenant.core.web; |
| | | |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import org.springframework.web.filter.OncePerRequestFilter; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | |
| | | /** |
| | | * 多租户 Context Web 过滤器 |
| | | * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TenantContextWebFilter extends OncePerRequestFilter { |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
| | | throws ServletException, IOException { |
| | | // 设置 |
| | | Long tenantId = WebFrameworkUtils.getTenantId(request); |
| | | if (tenantId != null) { |
| | | TenantContextHolder.setTenantId(tenantId); |
| | | } |
| | | try { |
| | | chain.doFilter(request, response); |
| | | } finally { |
| | | // 清理 |
| | | TenantContextHolder.clear(); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 多租户,支持如下层面: |
| | | * 1. DB:基于 MyBatis Plus 多租户的功能实现。 |
| | | * 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。 |
| | | * 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。 |
| | | * 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。 |
| | | * 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。 |
| | | * 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。 |
| | | * 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见: |
| | | * 1)Spring Async: |
| | | * {@link com.iailab.framework.quartz.config.IailabAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()} |
| | | * 2)Spring Security: |
| | | * TransmittableThreadLocalSecurityContextHolderStrategy |
| | | * 和 IailabSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法 |
| | | * |
| | | */ |
| | | package com.iailab.framework.tenant; |
对比新文件 |
| | |
| | | /* |
| | | * Copyright 2002-2021 the original author or authors. |
| | | * |
| | | * Licensed under the Apache License, Version 2.0 (the "License"); |
| | | * you may not use this file except in compliance with the License. |
| | | * You may obtain a copy of the License at |
| | | * |
| | | * https://www.apache.org/licenses/LICENSE-2.0 |
| | | * |
| | | * Unless required by applicable law or agreed to in writing, software |
| | | * distributed under the License is distributed on an "AS IS" BASIS, |
| | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| | | * See the License for the specific language governing permissions and |
| | | * limitations under the License. |
| | | */ |
| | | |
| | | package org.springframework.messaging.handler.invocation; |
| | | |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import com.iailab.framework.tenant.core.util.TenantUtils; |
| | | import org.springframework.core.DefaultParameterNameDiscoverer; |
| | | import org.springframework.core.MethodParameter; |
| | | import org.springframework.core.ParameterNameDiscoverer; |
| | | import org.springframework.core.ResolvableType; |
| | | import org.springframework.lang.Nullable; |
| | | import org.springframework.messaging.Message; |
| | | import org.springframework.messaging.handler.HandlerMethod; |
| | | import org.springframework.util.ObjectUtils; |
| | | |
| | | import java.lang.reflect.InvocationTargetException; |
| | | import java.lang.reflect.Method; |
| | | import java.lang.reflect.Type; |
| | | import java.util.Arrays; |
| | | |
| | | import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
| | | |
| | | /** |
| | | * Extension of {@link HandlerMethod} that invokes the underlying method with |
| | | * argument values resolved from the current HTTP request through a list of |
| | | * {@link HandlerMethodArgumentResolver}. |
| | | * |
| | | * 针对 rabbitmq-spring 和 kafka-spring,不存在合适的拓展点,可以实现 Consumer 消费前,读取 Header 中的 tenant-id 设置到 {@link TenantContextHolder} 中 |
| | | * TODO iailab:持续跟进,看看有没新的拓展点 |
| | | * |
| | | * @author Rossen Stoyanchev |
| | | * @author Juergen Hoeller |
| | | * @since 4.0 |
| | | */ |
| | | public class InvocableHandlerMethod extends HandlerMethod { |
| | | |
| | | private static final Object[] EMPTY_ARGS = new Object[0]; |
| | | |
| | | private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); |
| | | |
| | | private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); |
| | | |
| | | /** |
| | | * Create an instance from a {@code HandlerMethod}. |
| | | */ |
| | | public InvocableHandlerMethod(HandlerMethod handlerMethod) { |
| | | super(handlerMethod); |
| | | } |
| | | |
| | | /** |
| | | * Create an instance from a bean instance and a method. |
| | | */ |
| | | public InvocableHandlerMethod(Object bean, Method method) { |
| | | super(bean, method); |
| | | } |
| | | |
| | | /** |
| | | * Construct a new handler method with the given bean instance, method name and parameters. |
| | | * @param bean the object bean |
| | | * @param methodName the method name |
| | | * @param parameterTypes the method parameter types |
| | | * @throws NoSuchMethodException when the method cannot be found |
| | | */ |
| | | public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) |
| | | throws NoSuchMethodException { |
| | | |
| | | super(bean, methodName, parameterTypes); |
| | | } |
| | | |
| | | /** |
| | | * Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use for resolving method argument values. |
| | | */ |
| | | public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) { |
| | | this.resolvers = argumentResolvers; |
| | | } |
| | | |
| | | /** |
| | | * Set the ParameterNameDiscoverer for resolving parameter names when needed |
| | | * (e.g. default request attribute name). |
| | | * <p>Default is a {@link DefaultParameterNameDiscoverer}. |
| | | */ |
| | | public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { |
| | | this.parameterNameDiscoverer = parameterNameDiscoverer; |
| | | } |
| | | |
| | | /** |
| | | * Invoke the method after resolving its argument values in the context of the given message. |
| | | * <p>Argument values are commonly resolved through |
| | | * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. |
| | | * The {@code providedArgs} parameter however may supply argument values to be used directly, |
| | | * i.e. without argument resolution. |
| | | * <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the |
| | | * resolved arguments. |
| | | * @param message the current message being processed |
| | | * @param providedArgs "given" arguments matched by type, not resolved |
| | | * @return the raw value returned by the invoked method |
| | | * @throws Exception raised if no suitable argument resolver can be found, |
| | | * or if the method raised an exception |
| | | * @see #getMethodArgumentValues |
| | | * @see #doInvoke |
| | | */ |
| | | @Nullable |
| | | public Object invoke(Message<?> message, Object... providedArgs) throws Exception { |
| | | Object[] args = getMethodArgumentValues(message, providedArgs); |
| | | if (logger.isTraceEnabled()) { |
| | | logger.trace("Arguments: " + Arrays.toString(args)); |
| | | } |
| | | // 注意:如下是本类的改动点!!! |
| | | // 情况一:无租户编号的情况 |
| | | Long tenantId= parseTenantId(message); |
| | | if (tenantId == null) { |
| | | return doInvoke(args); |
| | | } |
| | | // 情况二:有租户的情况下 |
| | | return TenantUtils.execute(tenantId, () -> doInvoke(args)); |
| | | } |
| | | |
| | | private Long parseTenantId(Message<?> message) { |
| | | Object tenantId = message.getHeaders().get(HEADER_TENANT_ID); |
| | | if (tenantId == null) { |
| | | return null; |
| | | } |
| | | if (tenantId instanceof Long) { |
| | | return (Long) tenantId; |
| | | } |
| | | if (tenantId instanceof Number) { |
| | | return ((Number) tenantId).longValue(); |
| | | } |
| | | if (tenantId instanceof String) { |
| | | return Long.parseLong((String) tenantId); |
| | | } |
| | | if (tenantId instanceof byte[]) { |
| | | return Long.parseLong(new String((byte[]) tenantId)); |
| | | } |
| | | throw new IllegalArgumentException("未知的数据类型:" + tenantId); |
| | | } |
| | | |
| | | /** |
| | | * Get the method argument values for the current message, checking the provided |
| | | * argument values and falling back to the configured argument resolvers. |
| | | * <p>The resulting array will be passed into {@link #doInvoke}. |
| | | * @since 5.1.2 |
| | | */ |
| | | protected Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception { |
| | | MethodParameter[] parameters = getMethodParameters(); |
| | | if (ObjectUtils.isEmpty(parameters)) { |
| | | return EMPTY_ARGS; |
| | | } |
| | | |
| | | Object[] args = new Object[parameters.length]; |
| | | for (int i = 0; i < parameters.length; i++) { |
| | | MethodParameter parameter = parameters[i]; |
| | | parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); |
| | | args[i] = findProvidedArgument(parameter, providedArgs); |
| | | if (args[i] != null) { |
| | | continue; |
| | | } |
| | | if (!this.resolvers.supportsParameter(parameter)) { |
| | | throw new MethodArgumentResolutionException( |
| | | message, parameter, formatArgumentError(parameter, "No suitable resolver")); |
| | | } |
| | | try { |
| | | args[i] = this.resolvers.resolveArgument(parameter, message); |
| | | } |
| | | catch (Exception ex) { |
| | | // Leave stack trace for later, exception may actually be resolved and handled... |
| | | if (logger.isDebugEnabled()) { |
| | | String exMsg = ex.getMessage(); |
| | | if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { |
| | | logger.debug(formatArgumentError(parameter, exMsg)); |
| | | } |
| | | } |
| | | throw ex; |
| | | } |
| | | } |
| | | return args; |
| | | } |
| | | |
| | | /** |
| | | * Invoke the handler method with the given argument values. |
| | | */ |
| | | @Nullable |
| | | protected Object doInvoke(Object... args) throws Exception { |
| | | try { |
| | | return getBridgedMethod().invoke(getBean(), args); |
| | | } |
| | | catch (IllegalArgumentException ex) { |
| | | assertTargetBean(getBridgedMethod(), getBean(), args); |
| | | String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument"); |
| | | throw new IllegalStateException(formatInvokeError(text, args), ex); |
| | | } |
| | | catch (InvocationTargetException ex) { |
| | | // Unwrap for HandlerExceptionResolvers ... |
| | | Throwable targetException = ex.getTargetException(); |
| | | if (targetException instanceof RuntimeException) { |
| | | throw (RuntimeException) targetException; |
| | | } |
| | | else if (targetException instanceof Error) { |
| | | throw (Error) targetException; |
| | | } |
| | | else if (targetException instanceof Exception) { |
| | | throw (Exception) targetException; |
| | | } |
| | | else { |
| | | throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException); |
| | | } |
| | | } |
| | | } |
| | | |
| | | MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) { |
| | | return new AsyncResultMethodParameter(returnValue); |
| | | } |
| | | |
| | | private class AsyncResultMethodParameter extends HandlerMethodParameter { |
| | | |
| | | @Nullable |
| | | private final Object returnValue; |
| | | |
| | | private final ResolvableType returnType; |
| | | |
| | | public AsyncResultMethodParameter(@Nullable Object returnValue) { |
| | | super(-1); |
| | | this.returnValue = returnValue; |
| | | this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(); |
| | | } |
| | | |
| | | protected AsyncResultMethodParameter(AsyncResultMethodParameter original) { |
| | | super(original); |
| | | this.returnValue = original.returnValue; |
| | | this.returnType = original.returnType; |
| | | } |
| | | |
| | | @Override |
| | | public Class<?> getParameterType() { |
| | | if (this.returnValue != null) { |
| | | return this.returnValue.getClass(); |
| | | } |
| | | if (!ResolvableType.NONE.equals(this.returnType)) { |
| | | return this.returnType.toClass(); |
| | | } |
| | | return super.getParameterType(); |
| | | } |
| | | |
| | | @Override |
| | | public Type getGenericParameterType() { |
| | | return this.returnType.getType(); |
| | | } |
| | | |
| | | @Override |
| | | public AsyncResultMethodParameter clone() { |
| | | return new AsyncResultMethodParameter(this); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | org.springframework.boot.env.EnvironmentPostProcessor=\ |
| | | com.iailab.framework.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor |
对比新文件 |
| | |
| | | com.iailab.framework.tenant.config.IailabTenantRpcAutoConfiguration |
| | | com.iailab.framework.tenant.config.IailabTenantAutoConfiguration |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-env</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description> |
| | | 开发环境拓展,实现类似阿里的特性环境的能力 |
| | | 1. https://segmentfault.com/a/1190000018022987 |
| | | </description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <properties> |
| | | <maven.compiler.source>8</maven.compiler.source> |
| | | <maven.compiler.target>8</maven.compiler.target> |
| | | </properties> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Spring 核心 --> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Web 相关 --> |
| | | <dependency> |
| | | <groupId>org.springframework</groupId> |
| | | <artifactId>spring-web</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>jakarta.servlet</groupId> |
| | | <artifactId>jakarta.servlet-api</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- RPC 相关 --> |
| | | <dependency> |
| | | <groupId>org.springframework.cloud</groupId> |
| | | <artifactId>spring-cloud-loadbalancer</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>io.github.openfeign</groupId> |
| | | <artifactId>feign-core</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Registry 注册中心相关 --> |
| | | <dependency> |
| | | <groupId>com.alibaba.cloud</groupId> |
| | | <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.env.config; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.util.collection.SetUtils; |
| | | import com.iailab.framework.env.core.util.EnvUtils; |
| | | import org.springframework.boot.SpringApplication; |
| | | import org.springframework.boot.env.EnvironmentPostProcessor; |
| | | import org.springframework.core.env.ConfigurableEnvironment; |
| | | |
| | | import java.util.Set; |
| | | |
| | | import static com.iailab.framework.env.core.util.EnvUtils.HOST_NAME_VALUE; |
| | | |
| | | /** |
| | | * 多环境的 {@link EnvEnvironmentPostProcessor} 实现类 |
| | | * 将 iailab.env.tag 设置到 nacos 等组件对应的 tag 配置项,当且仅当它们不存在时 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class EnvEnvironmentPostProcessor implements EnvironmentPostProcessor { |
| | | |
| | | private static final Set<String> TARGET_TAG_KEYS = SetUtils.asSet( |
| | | "spring.cloud.nacos.discovery.metadata.tag" // Nacos 注册中心 |
| | | // MQ TODO |
| | | ); |
| | | |
| | | @Override |
| | | public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { |
| | | // 0. 设置 ${HOST_NAME} 兜底的环境变量 |
| | | String hostNameKey = StrUtil.subBetween(HOST_NAME_VALUE, "{", "}"); |
| | | if (!environment.containsProperty(hostNameKey)) { |
| | | environment.getSystemProperties().put(hostNameKey, EnvUtils.getHostName()); |
| | | } |
| | | |
| | | // 1.1 如果没有 iailab.env.tag 配置项,则不进行配置项的修改 |
| | | String tag = EnvUtils.getTag(environment); |
| | | if (StrUtil.isEmpty(tag)) { |
| | | return; |
| | | } |
| | | // 1.2 需要修改的配置项 |
| | | for (String targetTagKey : TARGET_TAG_KEYS) { |
| | | String targetTagValue = environment.getProperty(targetTagKey); |
| | | if (StrUtil.isNotEmpty(targetTagValue)) { |
| | | continue; |
| | | } |
| | | environment.getSystemProperties().put(targetTagKey, tag); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.env.config; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | |
| | | /** |
| | | * 环境配置 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @ConfigurationProperties(prefix = "iailab.env") |
| | | @Data |
| | | public class EnvProperties { |
| | | |
| | | public static final String TAG_KEY = "iailab.env.tag"; |
| | | |
| | | /** |
| | | * 环境标签 |
| | | */ |
| | | private String tag; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.env.config; |
| | | |
| | | import com.iailab.framework.env.core.fegin.EnvLoadBalancerClientFactory; |
| | | import com.iailab.framework.env.core.fegin.EnvRequestInterceptor; |
| | | import org.springframework.beans.factory.ObjectProvider; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; |
| | | import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification; |
| | | import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration; |
| | | import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 多环境的 RPC 组件的自动配置 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @EnableConfigurationProperties(EnvProperties.class) |
| | | public class IailabEnvRpcAutoConfiguration { |
| | | |
| | | // ========== Feign 相关 ========== |
| | | |
| | | /** |
| | | * 创建 {@link EnvLoadBalancerClientFactory} Bean |
| | | * |
| | | * 参考 {@link LoadBalancerAutoConfiguration#loadBalancerClientFactory(LoadBalancerClientsProperties)} 方法 |
| | | */ |
| | | @Bean |
| | | public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties, |
| | | ObjectProvider<List<LoadBalancerClientSpecification>> configurations) { |
| | | EnvLoadBalancerClientFactory clientFactory = new EnvLoadBalancerClientFactory(properties); |
| | | clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList)); |
| | | return clientFactory; |
| | | } |
| | | |
| | | @Bean |
| | | public EnvRequestInterceptor envRequestInterceptor() { |
| | | return new EnvRequestInterceptor(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.env.config; |
| | | |
| | | import com.iailab.framework.common.enums.WebFilterOrderEnum; |
| | | import com.iailab.framework.env.core.web.EnvWebFilter; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | /** |
| | | * 多环境的 Web 组件的自动配置 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) |
| | | @EnableConfigurationProperties(EnvProperties.class) |
| | | public class IailabEnvWebAutoConfiguration { |
| | | |
| | | /** |
| | | * 创建 {@link EnvWebFilter} Bean |
| | | */ |
| | | @Bean |
| | | public FilterRegistrationBean<EnvWebFilter> envWebFilterFilter() { |
| | | EnvWebFilter filter = new EnvWebFilter(); |
| | | FilterRegistrationBean<EnvWebFilter> bean = new FilterRegistrationBean<>(filter); |
| | | bean.setOrder(WebFilterOrderEnum.ENV_TAG_FILTER); |
| | | return bean; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.env.core.context; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.alibaba.ttl.TransmittableThreadLocal; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 开发环境上下文 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class EnvContextHolder { |
| | | |
| | | /** |
| | | * 标签的上下文 |
| | | * |
| | | * 使用 {@link List} 的原因,可能存在多层设置或者清理 |
| | | */ |
| | | private static final ThreadLocal<List<String>> TAG_CONTEXT = TransmittableThreadLocal.withInitial(ArrayList::new); |
| | | |
| | | public static void setTag(String tag) { |
| | | TAG_CONTEXT.get().add(tag); |
| | | } |
| | | |
| | | public static String getTag() { |
| | | return CollUtil.getLast(TAG_CONTEXT.get()); |
| | | } |
| | | |
| | | public static void removeTag() { |
| | | List<String> tags = TAG_CONTEXT.get(); |
| | | if (CollUtil.isEmpty(tags)) { |
| | | return; |
| | | } |
| | | tags.remove(tags.size() - 1); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.env.core.fegin; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.util.collection.CollectionUtils; |
| | | import com.iailab.framework.env.core.context.EnvContextHolder; |
| | | import com.iailab.framework.env.core.util.EnvUtils; |
| | | import com.alibaba.cloud.nacos.balancer.NacosBalancer; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.ObjectProvider; |
| | | import org.springframework.cloud.client.ServiceInstance; |
| | | import org.springframework.cloud.client.loadbalancer.DefaultResponse; |
| | | import org.springframework.cloud.client.loadbalancer.EmptyResponse; |
| | | import org.springframework.cloud.client.loadbalancer.Request; |
| | | import org.springframework.cloud.client.loadbalancer.Response; |
| | | import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; |
| | | import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier; |
| | | import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer; |
| | | import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; |
| | | import reactor.core.publisher.Mono; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 多环境的 {@link org.springframework.cloud.client.loadbalancer.LoadBalancerClient} 实现类 |
| | | * 在从服务实例列表选择时,优先选择 tag 匹配的服务实例 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | @Slf4j |
| | | public class EnvLoadBalancerClient implements ReactorServiceInstanceLoadBalancer { |
| | | |
| | | /** |
| | | * 用于获取 serviceId 对应的服务实例的列表 |
| | | */ |
| | | private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; |
| | | /** |
| | | * 需要获取的服务实例名 |
| | | * |
| | | * 暂时用于打印 logger 日志 |
| | | */ |
| | | private final String serviceId; |
| | | /** |
| | | * 被代理的 ReactiveLoadBalancer 对象 |
| | | */ |
| | | private final ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer; |
| | | |
| | | @Override |
| | | public Mono<Response<ServiceInstance>> choose(Request request) { |
| | | // 情况一,没有 tag 时,使用默认的 reactiveLoadBalancer 实现负载均衡 |
| | | String tag = EnvContextHolder.getTag(); |
| | | if (StrUtil.isEmpty(tag)) { |
| | | return Mono.from(reactiveLoadBalancer.choose(request)); |
| | | } |
| | | |
| | | // 情况二,有 tag 时,使用 tag 匹配服务实例 |
| | | ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new); |
| | | return supplier.get(request).next().map(list -> getInstanceResponse(list, tag)); |
| | | } |
| | | |
| | | private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, String tag) { |
| | | // 如果服务实例为空,则直接返回 |
| | | if (CollUtil.isEmpty(instances)) { |
| | | log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId); |
| | | return new EmptyResponse(); |
| | | } |
| | | |
| | | // 筛选满足条件的实例列表 |
| | | List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> tag.equals(EnvUtils.getTag(instance))); |
| | | if (CollUtil.isEmpty(chooseInstances)) { |
| | | log.warn("[getInstanceResponse][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag); |
| | | chooseInstances = instances; |
| | | } |
| | | |
| | | // TODO iailab:https://juejin.cn/post/7056770721858469896 想通网段 |
| | | |
| | | // 随机 + 权重获取实例列表 TODO iailab:目前直接使用 Nacos 提供的方法,如果替换注册中心,需要重新失败该方法 |
| | | return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.env.core.fegin; |
| | | |
| | | |
| | | import org.springframework.cloud.client.ServiceInstance; |
| | | import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; |
| | | import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; |
| | | import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; |
| | | import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; |
| | | |
| | | /** |
| | | * 多环境的 {@link LoadBalancerClientFactory} 实现类 |
| | | * 目的:在创建 {@link ReactiveLoadBalancer} 时,会额外增加 {@link EnvLoadBalancerClient} 代理,用于 tag 过滤服务实例 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class EnvLoadBalancerClientFactory extends LoadBalancerClientFactory { |
| | | |
| | | public EnvLoadBalancerClientFactory(LoadBalancerClientsProperties properties) { |
| | | super(properties); |
| | | } |
| | | |
| | | @Override |
| | | public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) { |
| | | ReactiveLoadBalancer<ServiceInstance> reactiveLoadBalancer = super.getInstance(serviceId); |
| | | // 参考 {@link com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancerClientConfiguration#nacosLoadBalancer(Environment, LoadBalancerClientFactory, NacosDiscoveryProperties)} 方法 |
| | | return new EnvLoadBalancerClient(super.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), |
| | | serviceId, reactiveLoadBalancer); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.env.core.fegin; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.env.core.context.EnvContextHolder; |
| | | import com.iailab.framework.env.core.util.EnvUtils; |
| | | import feign.RequestInterceptor; |
| | | import feign.RequestTemplate; |
| | | |
| | | /** |
| | | * 多环境的 {@link RequestInterceptor} 实现类:Feign 请求时,将 tag 设置到 header 中,继续透传给被调用的服务 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class EnvRequestInterceptor implements RequestInterceptor { |
| | | |
| | | @Override |
| | | public void apply(RequestTemplate requestTemplate) { |
| | | String tag = EnvContextHolder.getTag(); |
| | | if (StrUtil.isNotEmpty(tag)) { |
| | | EnvUtils.setTag(requestTemplate, tag); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.env.core; |
对比新文件 |
| | |
| | | package com.iailab.framework.env.core.util; |
| | | |
| | | import com.iailab.framework.env.config.EnvProperties; |
| | | import feign.RequestTemplate; |
| | | import lombok.SneakyThrows; |
| | | import org.springframework.cloud.client.ServiceInstance; |
| | | import org.springframework.core.env.Environment; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.net.InetAddress; |
| | | import java.util.Objects; |
| | | |
| | | /** |
| | | * 环境 Utils |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class EnvUtils { |
| | | |
| | | private static final String HEADER_TAG = "tag"; |
| | | |
| | | public static final String HOST_NAME_VALUE = "${HOSTNAME}"; |
| | | |
| | | public static String getTag(HttpServletRequest request) { |
| | | String tag = request.getHeader(HEADER_TAG); |
| | | // 如果请求的是 "${HOSTNAME}",则解析成对应的本地主机名 |
| | | // 目的:特殊逻辑,解决 IDEA Rest Client 不支持环境变量的读取,所以就服务器来做 |
| | | return Objects.equals(tag, HOST_NAME_VALUE) ? getHostName() : tag; |
| | | } |
| | | |
| | | public static String getTag(ServiceInstance instance) { |
| | | return instance.getMetadata().get(HEADER_TAG); |
| | | } |
| | | |
| | | public static String getTag(Environment environment) { |
| | | String tag = environment.getProperty(EnvProperties.TAG_KEY); |
| | | // 如果请求的是 "${HOSTNAME}",则解析成对应的本地主机名 |
| | | // 目的:特殊逻辑,解决 IDEA Rest Client 不支持环境变量的读取,所以就服务器来做 |
| | | return Objects.equals(tag, HOST_NAME_VALUE) ? getHostName() : tag; |
| | | } |
| | | |
| | | public static void setTag(RequestTemplate requestTemplate, String tag) { |
| | | requestTemplate.header(HEADER_TAG, tag); |
| | | } |
| | | |
| | | /** |
| | | * 获得 hostname 主机名 |
| | | * |
| | | * @return 主机名 |
| | | */ |
| | | @SneakyThrows |
| | | public static String getHostName() { |
| | | return InetAddress.getLocalHost().getHostName(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.env.core.web; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.env.core.context.EnvContextHolder; |
| | | import com.iailab.framework.env.core.util.EnvUtils; |
| | | import org.springframework.web.filter.OncePerRequestFilter; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | |
| | | /** |
| | | * 环境的 {@link javax.servlet.Filter} 实现类 |
| | | * 当有 tag 请求头时,设置到 {@link EnvContextHolder} 的标签上下文 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class EnvWebFilter extends OncePerRequestFilter { |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
| | | throws ServletException, IOException { |
| | | // 如果没有 tag,则走默认的流程 |
| | | String tag = EnvUtils.getTag(request); |
| | | if (StrUtil.isEmpty(tag)) { |
| | | chain.doFilter(request, response); |
| | | return; |
| | | } |
| | | |
| | | // 如果有 tag,则设置到上下文 |
| | | EnvContextHolder.setTag(tag); |
| | | try { |
| | | chain.doFilter(request, response); |
| | | } finally { |
| | | EnvContextHolder.removeTag(); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 开发环境拓展,实现类似阿里的特性环境的能力 |
| | | * 1. https://segmentfault.com/a/1190000018022987 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | package com.iailab.framework.env; |
对比新文件 |
| | |
| | | org.springframework.boot.env.EnvironmentPostProcessor=\ |
| | | com.iailab.framework.env.config.EnvEnvironmentPostProcessor |
对比新文件 |
| | |
| | | com.iailab.framework.env.config.IailabEnvWebAutoConfiguration |
| | | com.iailab.framework.env.config.IailabEnvRpcAutoConfiguration |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-excel</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>Excel 拓展</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Spring 核心 --> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- RPC 远程调用相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-rpc</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <!-- 业务组件 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-module-system-api</artifactId> <!-- 需要使用它,进行 Dict 的查询 --> |
| | | <version>${revision}</version> |
| | | </dependency> |
| | | |
| | | <!-- Web 相关 --> |
| | | <dependency> |
| | | <groupId>org.springframework</groupId> |
| | | <artifactId>spring-web</artifactId> |
| | | <scope>provided</scope> <!-- 设置为 provided,只有 ExcelUtils 使用 --> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>jakarta.servlet</groupId> |
| | | <artifactId>jakarta.servlet-api</artifactId> |
| | | <scope>provided</scope> <!-- 设置为 provided,只有 ExcelUtils 使用 --> |
| | | </dependency> |
| | | |
| | | <!-- 工具类相关 --> |
| | | <dependency> |
| | | <groupId>com.alibaba</groupId> |
| | | <artifactId>easyexcel</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.google.guava</groupId> |
| | | <artifactId>guava</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-biz-ip</artifactId> |
| | | <optional>true</optional> <!-- 设置为 optional,只有在 AreaConvert 的时候使用 --> |
| | | </dependency> |
| | | |
| | | <!-- Test 测试相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-test</artifactId> |
| | | <scope>test</scope> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.dict.config; |
| | | |
| | | import com.iailab.framework.dict.core.DictFrameworkUtils; |
| | | import com.iailab.module.system.api.dict.DictDataApi; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | @AutoConfiguration |
| | | public class IailabDictAutoConfiguration { |
| | | |
| | | @Bean |
| | | @SuppressWarnings("InstantiationOfUtilityClass") |
| | | public DictFrameworkUtils dictUtils(DictDataApi dictDataApi) { |
| | | DictFrameworkUtils.init(dictDataApi); |
| | | return new DictFrameworkUtils(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.dict.config; |
| | | |
| | | import com.iailab.module.system.api.dict.DictDataApi; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.cloud.openfeign.EnableFeignClients; |
| | | |
| | | /** |
| | | * 字典用到 Feign 的配置项 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @EnableFeignClients(clients = DictDataApi.class) // 主要是引入相关的 API 服务 |
| | | public class IailabDictRpcAutoConfiguration { |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.dict.core; |
| | | |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import com.iailab.framework.common.core.KeyValue; |
| | | import com.iailab.framework.common.util.cache.CacheUtils; |
| | | import com.iailab.module.system.api.dict.DictDataApi; |
| | | import com.iailab.module.system.api.dict.dto.DictDataRespDTO; |
| | | import com.google.common.cache.CacheLoader; |
| | | import com.google.common.cache.LoadingCache; |
| | | import lombok.SneakyThrows; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.time.Duration; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 字典工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class DictFrameworkUtils { |
| | | |
| | | private static DictDataApi dictDataApi; |
| | | |
| | | private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO(); |
| | | |
| | | // TODO @puhui999:GET_DICT_DATA_CACHE、GET_DICT_DATA_LIST_CACHE、PARSE_DICT_DATA_CACHE 这 3 个缓存是有点重叠,可以思考下,有没可能减少 1 个。微信讨论好私聊,再具体改哈 |
| | | /** |
| | | * 针对 {@link #getDictDataLabel(String, String)} 的缓存 |
| | | */ |
| | | private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( |
| | | Duration.ofMinutes(1L), // 过期时间 1 分钟 |
| | | new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() { |
| | | |
| | | @Override |
| | | public DictDataRespDTO load(KeyValue<String, String> key) { |
| | | return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()).getCheckedData(), DICT_DATA_NULL); |
| | | } |
| | | |
| | | }); |
| | | |
| | | /** |
| | | * 针对 {@link #getDictDataLabelList(String)} 的缓存 |
| | | */ |
| | | private static final LoadingCache<String, List<String>> GET_DICT_DATA_LIST_CACHE = CacheUtils.buildAsyncReloadingCache( |
| | | Duration.ofMinutes(1L), // 过期时间 1 分钟 |
| | | new CacheLoader<String, List<String>>() { |
| | | |
| | | @Override |
| | | public List<String> load(String dictType) { |
| | | return dictDataApi.getDictDataLabelList(dictType); |
| | | } |
| | | |
| | | }); |
| | | |
| | | /** |
| | | * 针对 {@link #parseDictDataValue(String, String)} 的缓存 |
| | | */ |
| | | private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( |
| | | Duration.ofMinutes(1L), // 过期时间 1 分钟 |
| | | new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() { |
| | | |
| | | @Override |
| | | public DictDataRespDTO load(KeyValue<String, String> key) { |
| | | return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()).getCheckedData(), DICT_DATA_NULL); |
| | | } |
| | | |
| | | }); |
| | | |
| | | public static void init(DictDataApi dictDataApi) { |
| | | DictFrameworkUtils.dictDataApi = dictDataApi; |
| | | log.info("[init][初始化 DictFrameworkUtils 成功]"); |
| | | } |
| | | |
| | | @SneakyThrows |
| | | public static String getDictDataLabel(String dictType, Integer value) { |
| | | return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, String.valueOf(value))).getLabel(); |
| | | } |
| | | |
| | | @SneakyThrows |
| | | public static String getDictDataLabel(String dictType, String value) { |
| | | return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel(); |
| | | } |
| | | |
| | | @SneakyThrows |
| | | public static List<String> getDictDataLabelList(String dictType) { |
| | | return GET_DICT_DATA_LIST_CACHE.get(dictType); |
| | | } |
| | | |
| | | @SneakyThrows |
| | | public static String parseDictDataValue(String dictType, String label) { |
| | | return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 字典数据模块,提供 {@link com.iailab.framework.dict.core.DictFrameworkUtils} 工具类 |
| | | * |
| | | * 通过将字典缓存在内存中,保证性能 |
| | | */ |
| | | package com.iailab.framework.dict; |
对比新文件 |
| | |
| | | package com.iailab.framework.excel.core.annotations; |
| | | |
| | | import java.lang.annotation.*; |
| | | |
| | | /** |
| | | * 字典格式化 |
| | | * |
| | | * 实现将字典数据的值,格式化成字典数据的标签 |
| | | */ |
| | | @Target({ElementType.FIELD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Inherited |
| | | public @interface DictFormat { |
| | | |
| | | /** |
| | | * 例如说,SysDictTypeConstants、InfDictTypeConstants |
| | | * |
| | | * @return 字典类型 |
| | | */ |
| | | String value(); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.excel.core.annotations; |
| | | |
| | | import java.lang.annotation.*; |
| | | |
| | | /** |
| | | * 给 Excel 列添加下拉选择数据 |
| | | * |
| | | * 其中 {@link #dictType()} 和 {@link #functionName()} 二选一 |
| | | * |
| | | * @author HUIHUI |
| | | */ |
| | | @Target({ElementType.FIELD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Inherited |
| | | public @interface ExcelColumnSelect { |
| | | |
| | | /** |
| | | * @return 字典类型 |
| | | */ |
| | | String dictType() default ""; |
| | | |
| | | /** |
| | | * @return 获取下拉数据源的方法名称 |
| | | */ |
| | | String functionName() default ""; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.excel.core.convert; |
| | | |
| | | import cn.hutool.core.convert.Convert; |
| | | import com.iailab.framework.ip.core.Area; |
| | | import com.iailab.framework.ip.core.utils.AreaUtils; |
| | | import com.alibaba.excel.converters.Converter; |
| | | import com.alibaba.excel.enums.CellDataTypeEnum; |
| | | import com.alibaba.excel.metadata.GlobalConfiguration; |
| | | import com.alibaba.excel.metadata.data.ReadCellData; |
| | | import com.alibaba.excel.metadata.property.ExcelContentProperty; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | /** |
| | | * Excel 数据地区转换器 |
| | | * |
| | | * @author HUIHUI |
| | | */ |
| | | @Slf4j |
| | | public class AreaConvert implements Converter<Object> { |
| | | |
| | | @Override |
| | | public Class<?> supportJavaTypeKey() { |
| | | throw new UnsupportedOperationException("暂不支持,也不需要"); |
| | | } |
| | | |
| | | @Override |
| | | public CellDataTypeEnum supportExcelTypeKey() { |
| | | throw new UnsupportedOperationException("暂不支持,也不需要"); |
| | | } |
| | | |
| | | @Override |
| | | public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, |
| | | GlobalConfiguration globalConfiguration) { |
| | | // 解析地区编号 |
| | | String label = readCellData.getStringValue(); |
| | | Area area = AreaUtils.parseArea(label); |
| | | if (area == null) { |
| | | log.error("[convertToJavaData][label({}) 解析不掉]", label); |
| | | return null; |
| | | } |
| | | // 将 value 转换成对应的属性 |
| | | Class<?> fieldClazz = contentProperty.getField().getType(); |
| | | return Convert.convert(fieldClazz, area.getId()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.excel.core.convert; |
| | | |
| | | import cn.hutool.core.convert.Convert; |
| | | import com.iailab.framework.dict.core.DictFrameworkUtils; |
| | | import com.iailab.framework.excel.core.annotations.DictFormat; |
| | | import com.alibaba.excel.converters.Converter; |
| | | import com.alibaba.excel.enums.CellDataTypeEnum; |
| | | import com.alibaba.excel.metadata.GlobalConfiguration; |
| | | import com.alibaba.excel.metadata.data.ReadCellData; |
| | | import com.alibaba.excel.metadata.data.WriteCellData; |
| | | import com.alibaba.excel.metadata.property.ExcelContentProperty; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | /** |
| | | * Excel 数据字典转换器 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class DictConvert implements Converter<Object> { |
| | | |
| | | @Override |
| | | public Class<?> supportJavaTypeKey() { |
| | | throw new UnsupportedOperationException("暂不支持,也不需要"); |
| | | } |
| | | |
| | | @Override |
| | | public CellDataTypeEnum supportExcelTypeKey() { |
| | | throw new UnsupportedOperationException("暂不支持,也不需要"); |
| | | } |
| | | |
| | | @Override |
| | | public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, |
| | | GlobalConfiguration globalConfiguration) { |
| | | // 使用字典解析 |
| | | String type = getType(contentProperty); |
| | | String label = readCellData.getStringValue(); |
| | | String value = DictFrameworkUtils.parseDictDataValue(type, label); |
| | | if (value == null) { |
| | | log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label); |
| | | return null; |
| | | } |
| | | // 将 String 的 value 转换成对应的属性 |
| | | Class<?> fieldClazz = contentProperty.getField().getType(); |
| | | return Convert.convert(fieldClazz, value); |
| | | } |
| | | |
| | | @Override |
| | | public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, |
| | | GlobalConfiguration globalConfiguration) { |
| | | // 空时,返回空 |
| | | if (object == null) { |
| | | return new WriteCellData<>(""); |
| | | } |
| | | |
| | | // 使用字典格式化 |
| | | String type = getType(contentProperty); |
| | | String value = String.valueOf(object); |
| | | String label = DictFrameworkUtils.getDictDataLabel(type, value); |
| | | if (label == null) { |
| | | log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value); |
| | | return new WriteCellData<>(""); |
| | | } |
| | | // 生成 Excel 小表格 |
| | | return new WriteCellData<>(label); |
| | | } |
| | | |
| | | private static String getType(ExcelContentProperty contentProperty) { |
| | | return contentProperty.getField().getAnnotation(DictFormat.class).value(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.excel.core.convert; |
| | | |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.alibaba.excel.converters.Converter; |
| | | import com.alibaba.excel.enums.CellDataTypeEnum; |
| | | import com.alibaba.excel.metadata.GlobalConfiguration; |
| | | import com.alibaba.excel.metadata.data.WriteCellData; |
| | | import com.alibaba.excel.metadata.property.ExcelContentProperty; |
| | | |
| | | /** |
| | | * Excel Json 转换器 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class JsonConvert implements Converter<Object> { |
| | | |
| | | @Override |
| | | public Class<?> supportJavaTypeKey() { |
| | | throw new UnsupportedOperationException("暂不支持,也不需要"); |
| | | } |
| | | |
| | | @Override |
| | | public CellDataTypeEnum supportExcelTypeKey() { |
| | | throw new UnsupportedOperationException("暂不支持,也不需要"); |
| | | } |
| | | |
| | | @Override |
| | | public WriteCellData<String> convertToExcelData(Object value, ExcelContentProperty contentProperty, |
| | | GlobalConfiguration globalConfiguration) { |
| | | // 生成 Excel 小表格 |
| | | return new WriteCellData<>(JsonUtils.toJsonString(value)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.excel.core.convert; |
| | | |
| | | import com.alibaba.excel.converters.Converter; |
| | | import com.alibaba.excel.enums.CellDataTypeEnum; |
| | | import com.alibaba.excel.metadata.GlobalConfiguration; |
| | | import com.alibaba.excel.metadata.data.WriteCellData; |
| | | import com.alibaba.excel.metadata.property.ExcelContentProperty; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | |
| | | /** |
| | | * 金额转换器 |
| | | * |
| | | * 金额单位:分 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class MoneyConvert implements Converter<Integer> { |
| | | |
| | | @Override |
| | | public Class<?> supportJavaTypeKey() { |
| | | throw new UnsupportedOperationException("暂不支持,也不需要"); |
| | | } |
| | | |
| | | @Override |
| | | public CellDataTypeEnum supportExcelTypeKey() { |
| | | throw new UnsupportedOperationException("暂不支持,也不需要"); |
| | | } |
| | | |
| | | @Override |
| | | public WriteCellData<String> convertToExcelData(Integer value, ExcelContentProperty contentProperty, |
| | | GlobalConfiguration globalConfiguration) { |
| | | BigDecimal result = BigDecimal.valueOf(value) |
| | | .divide(new BigDecimal(100), 2, RoundingMode.HALF_UP); |
| | | return new WriteCellData<>(result.toString()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.excel.core.function; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * Excel 列下拉数据源获取接口 |
| | | * |
| | | * 为什么不直接解析字典还搞个接口?考虑到有的下拉数据不是从字典中获取的所有需要做一个兼容 |
| | | |
| | | * @author HUIHUI |
| | | */ |
| | | public interface ExcelColumnSelectFunction { |
| | | |
| | | /** |
| | | * 获得方法名称 |
| | | * |
| | | * @return 方法名称 |
| | | */ |
| | | String getName(); |
| | | |
| | | /** |
| | | * 获得列下拉数据源 |
| | | * |
| | | * @return 下拉数据源 |
| | | */ |
| | | List<String> getOptions(); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.excel.core.handler; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.lang.Assert; |
| | | import cn.hutool.core.map.MapUtil; |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.extra.spring.SpringUtil; |
| | | import cn.hutool.poi.excel.ExcelUtil; |
| | | import com.alibaba.excel.annotation.ExcelProperty; |
| | | import com.alibaba.excel.write.handler.SheetWriteHandler; |
| | | import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; |
| | | import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; |
| | | import com.iailab.framework.common.core.KeyValue; |
| | | import com.iailab.framework.dict.core.DictFrameworkUtils; |
| | | import com.iailab.framework.excel.core.annotations.ExcelColumnSelect; |
| | | import com.iailab.framework.excel.core.function.ExcelColumnSelectFunction; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.hssf.usermodel.HSSFDataValidation; |
| | | import org.apache.poi.ss.usermodel.*; |
| | | import org.apache.poi.ss.util.CellRangeAddressList; |
| | | |
| | | import java.lang.reflect.Field; |
| | | import java.util.*; |
| | | |
| | | import static com.iailab.framework.common.util.collection.CollectionUtils.convertList; |
| | | |
| | | /** |
| | | * 基于固定 sheet 实现下拉框 |
| | | * |
| | | * @author HUIHUI |
| | | */ |
| | | @Slf4j |
| | | public class SelectSheetWriteHandler implements SheetWriteHandler { |
| | | |
| | | /** |
| | | * 数据起始行从 0 开始 |
| | | * 约定:本项目第一行有标题所以从 1 开始如果您的 Excel 有多行标题请自行更改 |
| | | */ |
| | | public static final int FIRST_ROW = 1; |
| | | /** |
| | | * 下拉列需要创建下拉框的行数,默认两千行如需更多请自行调整 |
| | | */ |
| | | public static final int LAST_ROW = 2000; |
| | | |
| | | private static final String DICT_SHEET_NAME = "字典sheet"; |
| | | |
| | | /** |
| | | * key: 列 value: 下拉数据源 |
| | | */ |
| | | private final Map<Integer, List<String>> selectMap = new HashMap<>(); |
| | | |
| | | private static Boolean ifSetSelect; |
| | | |
| | | public SelectSheetWriteHandler(Class<?> head, Boolean selectFlag) { |
| | | ifSetSelect = selectFlag; |
| | | // 加载下拉数据获取接口 |
| | | Map<String, ExcelColumnSelectFunction> beansMap = SpringUtil.getBeanFactory().getBeansOfType(ExcelColumnSelectFunction.class); |
| | | if (MapUtil.isEmpty(beansMap)) { |
| | | return; |
| | | } |
| | | List<Field> fields = new ArrayList<>(); |
| | | for (Class<?> c = head; c != null; c = c.getSuperclass()) { |
| | | Collections.addAll(fields, c.getDeclaredFields()); |
| | | } |
| | | // 解析下拉数据 |
| | | int colIndex = 0; |
| | | for (Field field : fields) { |
| | | if (field.isAnnotationPresent(ExcelColumnSelect.class)) { |
| | | ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); |
| | | if (excelProperty != null && excelProperty.index() != -1) { |
| | | getSelectDataList(excelProperty.index(), field); |
| | | }else{ |
| | | getSelectDataList(colIndex, field); |
| | | } |
| | | } |
| | | colIndex++; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获得下拉数据,并添加到 {@link #selectMap} 中 |
| | | * |
| | | * @param colIndex 列索引 |
| | | * @param field 字段 |
| | | */ |
| | | private void getSelectDataList(int colIndex, Field field) { |
| | | ExcelColumnSelect columnSelect = field.getAnnotation(ExcelColumnSelect.class); |
| | | String dictType = columnSelect.dictType(); |
| | | String functionName = columnSelect.functionName(); |
| | | Assert.isTrue(ObjectUtil.isNotEmpty(dictType) || ObjectUtil.isNotEmpty(functionName), |
| | | "Field({}) 的 @ExcelColumnSelect 注解,dictType 和 functionName 不能同时为空", field.getName()); |
| | | |
| | | // 情况一:使用 dictType 获得下拉数据 |
| | | if (StrUtil.isNotEmpty(dictType)) { // 情况一: 字典数据 (默认) |
| | | selectMap.put(colIndex, DictFrameworkUtils.getDictDataLabelList(dictType)); |
| | | return; |
| | | } |
| | | |
| | | // 情况二:使用 functionName 获得下拉数据 |
| | | Map<String, ExcelColumnSelectFunction> functionMap = SpringUtil.getApplicationContext().getBeansOfType(ExcelColumnSelectFunction.class); |
| | | ExcelColumnSelectFunction function = CollUtil.findOne(functionMap.values(), item -> item.getName().equals(functionName)); |
| | | Assert.notNull(function, "未找到对应的 function({})", functionName); |
| | | selectMap.put(colIndex, function.getOptions()); |
| | | } |
| | | |
| | | @Override |
| | | public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { |
| | | if (CollUtil.isEmpty(selectMap)) { |
| | | return; |
| | | } |
| | | // 1. 获取相应操作对象 |
| | | DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper(); // 需要设置下拉框的 sheet 页的数据验证助手 |
| | | Workbook workbook = writeWorkbookHolder.getWorkbook(); // 获得工作簿 |
| | | List<KeyValue<Integer, List<String>>> keyValues = convertList(selectMap.entrySet(), entry -> new KeyValue<>(entry.getKey(), entry.getValue())); |
| | | keyValues.sort(Comparator.comparing(item -> item.getValue().size())); // 升序不然创建下拉会报错 |
| | | if (ifSetSelect){ |
| | | for (KeyValue<Integer, List<String>> keyValue : keyValues) { |
| | | /*起始行、终止行、起始列、终止列 起始行为1即表示表头不设置**/ |
| | | CellRangeAddressList addressList = new CellRangeAddressList(FIRST_ROW, LAST_ROW, keyValue.getKey(), keyValue.getKey()); |
| | | /*设置下拉框数据**/ |
| | | DataValidationConstraint constraint = helper.createExplicitListConstraint(keyValue.getValue().toArray(new String[0])); |
| | | DataValidation dataValidation = helper.createValidation(constraint, addressList); |
| | | if (dataValidation instanceof HSSFDataValidation) { |
| | | dataValidation.setSuppressDropDownArrow(false); |
| | | } else { |
| | | dataValidation.setSuppressDropDownArrow(true); |
| | | dataValidation.setShowErrorBox(true); |
| | | } |
| | | // 2.2 阻止输入非下拉框的值 |
| | | dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP); |
| | | dataValidation.createErrorBox("提示", "此值不存在于下拉选择中!"); |
| | | writeSheetHolder.getSheet().addValidationData(dataValidation); |
| | | } |
| | | }else{ |
| | | // 2. 创建数据字典的 sheet 页 |
| | | Sheet dictSheet = workbook.createSheet(DICT_SHEET_NAME); |
| | | for (KeyValue<Integer, List<String>> keyValue : keyValues) { |
| | | int rowLength = keyValue.getValue().size(); |
| | | // 2.1 设置字典 sheet 页的值,每一列一部字典项 |
| | | for (int i = 0; i < rowLength; i++) { |
| | | Row row = dictSheet.getRow(i); |
| | | if (row == null) { |
| | | row = dictSheet.createRow(i); |
| | | } |
| | | row.createCell(keyValue.getKey()).setCellValue(keyValue.getValue().get(i)); |
| | | } |
| | | // 2.2 设置单元格下拉选择 |
| | | setColumnSelect(writeSheetHolder, workbook, helper, keyValue); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 设置单元格下拉选择 |
| | | */ |
| | | private static void setColumnSelect(WriteSheetHolder writeSheetHolder, Workbook workbook, DataValidationHelper helper, |
| | | KeyValue<Integer, List<String>> keyValue) { |
| | | // 1.1 创建可被其他单元格引用的名称 |
| | | Name name = workbook.createName(); |
| | | String excelColumn = ExcelUtil.indexToColName(keyValue.getKey()); |
| | | // 1.2 下拉框数据来源 eg:字典sheet!$B1:$B2 |
| | | String refers = DICT_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + keyValue.getValue().size(); |
| | | name.setNameName("dict" + keyValue.getKey()); // 设置名称的名字 |
| | | name.setRefersToFormula(refers); // 设置公式 |
| | | |
| | | // 2.1 设置约束 |
| | | DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey()); // 设置引用约束 |
| | | // 设置下拉单元格的首行、末行、首列、末列 |
| | | CellRangeAddressList rangeAddressList = new CellRangeAddressList(FIRST_ROW, LAST_ROW, |
| | | keyValue.getKey(), keyValue.getKey()); |
| | | DataValidation validation = helper.createValidation(constraint, rangeAddressList); |
| | | if (validation instanceof HSSFDataValidation) { |
| | | validation.setSuppressDropDownArrow(false); |
| | | } else { |
| | | validation.setSuppressDropDownArrow(true); |
| | | validation.setShowErrorBox(true); |
| | | } |
| | | // 2.2 阻止输入非下拉框的值 |
| | | validation.setErrorStyle(DataValidation.ErrorStyle.STOP); |
| | | validation.createErrorBox("提示", "此值不存在于下拉选择中!"); |
| | | // 2.3 添加下拉框约束 |
| | | writeSheetHolder.getSheet().addValidationData(validation); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.excel.core.util; |
| | | |
| | | import com.iailab.framework.excel.core.handler.SelectSheetWriteHandler; |
| | | import com.alibaba.excel.EasyExcel; |
| | | import com.alibaba.excel.converters.longconverter.LongStringConverter; |
| | | import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; |
| | | 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.List; |
| | | |
| | | /** |
| | | * Excel 工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class ExcelUtils { |
| | | |
| | | /** |
| | | * 将列表以 Excel 响应给前端 |
| | | * |
| | | * @param response 响应 |
| | | * @param filename 文件名 |
| | | * @param sheetName Excel sheet 名 |
| | | * @param head Excel head 头 |
| | | * @param data 数据列表哦 |
| | | * @param <T> 泛型,保证 head 和 data 类型的一致性 |
| | | * @throws IOException 写入失败的情况 |
| | | */ |
| | | public static <T> void write(HttpServletResponse response, String filename, String sheetName, |
| | | Class<T> head, List<T> data) throws IOException { |
| | | // 输出 Excel |
| | | EasyExcel.write(response.getOutputStream(), head) |
| | | .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 |
| | | .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 |
| | | .registerWriteHandler(new SelectSheetWriteHandler(head,false)) // 基于固定 sheet 实现下拉框 |
| | | .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 |
| | | .sheet(sheetName).doWrite(data); |
| | | // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 |
| | | response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); |
| | | response.setContentType("application/vnd.ms-excel;charset=UTF-8"); |
| | | } |
| | | |
| | | public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException { |
| | | return EasyExcel.read(file.getInputStream(), head, null) |
| | | .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 |
| | | .doReadAllSync(); |
| | | } |
| | | |
| | | public static <T> void write(HttpServletResponse response, String filename, String sheetName, |
| | | Class<T> head, List<T> data, boolean selectFlag) throws IOException { |
| | | // 输出 Excel |
| | | EasyExcel.write(response.getOutputStream(), head) |
| | | .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 |
| | | .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 |
| | | .registerWriteHandler(new SelectSheetWriteHandler(head,selectFlag)) // 基于固定 sheet 实现下拉框 |
| | | .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 |
| | | .sheet(sheetName).doWrite(data); |
| | | // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 |
| | | response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); |
| | | response.setContentType("application/vnd.ms-excel;charset=UTF-8"); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 基于 EasyExcel 实现 Excel 相关的操作 |
| | | */ |
| | | package com.iailab.framework.excel; |
对比新文件 |
| | |
| | | com.iailab.framework.dict.config.IailabDictRpcAutoConfiguration |
| | | com.iailab.framework.dict.config.IailabDictAutoConfiguration |
对比新文件 |
| | |
| | | package com.iailab.framework.dict.core.util; |
| | | |
| | | import com.iailab.framework.common.enums.CommonStatusEnum; |
| | | import com.iailab.framework.dict.core.DictFrameworkUtils; |
| | | import com.iailab.framework.test.core.ut.BaseMockitoUnitTest; |
| | | import com.iailab.module.system.api.dict.DictDataApi; |
| | | import com.iailab.module.system.api.dict.dto.DictDataRespDTO; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.Mock; |
| | | |
| | | import static com.iailab.framework.common.pojo.CommonResult.success; |
| | | import static com.iailab.framework.test.core.util.RandomUtils.randomPojo; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | /** |
| | | * {@link DictFrameworkUtils} 的单元测试 |
| | | */ |
| | | public class DictFrameworkUtilsTest extends BaseMockitoUnitTest { |
| | | |
| | | @Mock |
| | | private DictDataApi dictDataApi; |
| | | |
| | | @BeforeEach |
| | | public void setUp() { |
| | | DictFrameworkUtils.init(dictDataApi); |
| | | } |
| | | |
| | | @Test |
| | | public void testGetDictDataLabel() { |
| | | // mock 数据 |
| | | DictDataRespDTO dataRespDTO = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); |
| | | // mock 方法 |
| | | when(dictDataApi.getDictData(dataRespDTO.getDictType(), dataRespDTO.getValue())).thenReturn(success(dataRespDTO)); |
| | | |
| | | // 断言返回值 |
| | | assertEquals(dataRespDTO.getLabel(), DictFrameworkUtils.getDictDataLabel(dataRespDTO.getDictType(), dataRespDTO.getValue())); |
| | | } |
| | | |
| | | @Test |
| | | public void testParseDictDataValue() { |
| | | // mock 数据 |
| | | DictDataRespDTO resp = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); |
| | | // mock 方法 |
| | | when(dictDataApi.parseDictData(resp.getDictType(), resp.getLabel())).thenReturn(success(resp)); |
| | | // 断言返回值 |
| | | assertEquals(resp.getValue(), DictFrameworkUtils.parseDictDataValue(resp.getDictType(), resp.getLabel())); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-job</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>任务拓展,基于 XXL-Job 实现</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Spring 核心 --> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-configuration-processor</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <!-- Job 相关 --> |
| | | <dependency> |
| | | <groupId>com.xuxueli</groupId> |
| | | <artifactId>xxl-job-core</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- 工具类相关 --> |
| | | <dependency> |
| | | <groupId>jakarta.validation</groupId> |
| | | <artifactId>jakarta.validation-api</artifactId> |
| | | </dependency> |
| | | |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.quartz.config; |
| | | |
| | | import com.alibaba.ttl.TtlRunnable; |
| | | import org.springframework.beans.BeansException; |
| | | import org.springframework.beans.factory.config.BeanPostProcessor; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.scheduling.annotation.EnableAsync; |
| | | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; |
| | | |
| | | /** |
| | | * 异步任务 Configuration |
| | | */ |
| | | @AutoConfiguration |
| | | @EnableAsync |
| | | public class IailabAsyncAutoConfiguration { |
| | | |
| | | @Bean |
| | | public BeanPostProcessor threadPoolTaskExecutorBeanPostProcessor() { |
| | | return new BeanPostProcessor() { |
| | | |
| | | @Override |
| | | public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { |
| | | if (!(bean instanceof ThreadPoolTaskExecutor)) { |
| | | return bean; |
| | | } |
| | | // 修改提交的任务,接入 TransmittableThreadLocal |
| | | ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; |
| | | executor.setTaskDecorator(TtlRunnable::get); |
| | | return executor; |
| | | } |
| | | |
| | | }; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.quartz.config; |
| | | |
| | | import com.xxl.job.core.executor.XxlJobExecutor; |
| | | import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.scheduling.annotation.EnableScheduling; |
| | | |
| | | /** |
| | | * XXL-Job 自动配置类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @ConditionalOnClass(XxlJobSpringExecutor.class) |
| | | @ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true", matchIfMissing = true) |
| | | @EnableConfigurationProperties({XxlJobProperties.class}) |
| | | @EnableScheduling // 开启 Spring 自带的定时任务 |
| | | @Slf4j |
| | | public class IailabXxlJobAutoConfiguration { |
| | | |
| | | @Bean |
| | | @ConditionalOnMissingBean |
| | | public XxlJobExecutor xxlJobExecutor(XxlJobProperties properties) { |
| | | log.info("[xxlJobExecutor][初始化 XXL-Job 执行器的配置]"); |
| | | XxlJobProperties.AdminProperties admin = properties.getAdmin(); |
| | | XxlJobProperties.ExecutorProperties executor = properties.getExecutor(); |
| | | |
| | | // 初始化执行器 |
| | | XxlJobExecutor xxlJobExecutor = new XxlJobSpringExecutor(); |
| | | xxlJobExecutor.setIp(executor.getIp()); |
| | | xxlJobExecutor.setPort(executor.getPort()); |
| | | xxlJobExecutor.setAppname(executor.getAppName()); |
| | | xxlJobExecutor.setLogPath(executor.getLogPath()); |
| | | xxlJobExecutor.setLogRetentionDays(executor.getLogRetentionDays()); |
| | | xxlJobExecutor.setAdminAddresses(admin.getAddresses()); |
| | | xxlJobExecutor.setAccessToken(properties.getAccessToken()); |
| | | return xxlJobExecutor; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.quartz.config; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import javax.validation.Valid; |
| | | import javax.validation.constraints.NotEmpty; |
| | | import javax.validation.constraints.NotNull; |
| | | |
| | | /** |
| | | * XXL-Job 配置类 |
| | | */ |
| | | @ConfigurationProperties("xxl.job") |
| | | @Validated |
| | | @Data |
| | | public class XxlJobProperties { |
| | | |
| | | /** |
| | | * 是否开启,默认为 true 关闭 |
| | | */ |
| | | private Boolean enabled = true; |
| | | /** |
| | | * 访问令牌 |
| | | */ |
| | | private String accessToken; |
| | | /** |
| | | * 控制器配置 |
| | | */ |
| | | @NotNull(message = "控制器配置不能为空") |
| | | private AdminProperties admin; |
| | | /** |
| | | * 执行器配置 |
| | | */ |
| | | @NotNull(message = "执行器配置不能为空") |
| | | private ExecutorProperties executor; |
| | | |
| | | /** |
| | | * XXL-Job 调度器配置类 |
| | | */ |
| | | @Data |
| | | @Valid |
| | | public static class AdminProperties { |
| | | |
| | | /** |
| | | * 调度器地址 |
| | | */ |
| | | @NotEmpty(message = "调度器地址不能为空") |
| | | private String addresses; |
| | | |
| | | } |
| | | |
| | | /** |
| | | * XXL-Job 执行器配置类 |
| | | */ |
| | | @Data |
| | | @Valid |
| | | public static class ExecutorProperties { |
| | | |
| | | /** |
| | | * 默认端口 |
| | | * |
| | | * 这里使用 -1 表示随机 |
| | | */ |
| | | private static final Integer PORT_DEFAULT = -1; |
| | | |
| | | /** |
| | | * 默认日志保留天数 |
| | | * |
| | | * 如果想永久保留,则设置为 -1 |
| | | */ |
| | | private static final Integer LOG_RETENTION_DAYS_DEFAULT = 30; |
| | | |
| | | /** |
| | | * 应用名 |
| | | */ |
| | | @NotEmpty(message = "应用名不能为空") |
| | | private String appName; |
| | | /** |
| | | * 执行器的 IP |
| | | */ |
| | | private String ip; |
| | | /** |
| | | * 执行器的 Port |
| | | */ |
| | | private Integer port = PORT_DEFAULT; |
| | | /** |
| | | * 日志地址 |
| | | */ |
| | | @NotEmpty(message = "日志地址不能为空") |
| | | private String logPath; |
| | | /** |
| | | * 日志保留天数 |
| | | */ |
| | | private Integer logRetentionDays = LOG_RETENTION_DAYS_DEFAULT; |
| | | |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 1. 定时任务,基于 XXL-Job 实现。 |
| | | * 2. 异步任务,采用 Spring Async 异步执行。 |
| | | */ |
| | | package com.iailab.framework.quartz; |
对比新文件 |
| | |
| | | com.iailab.framework.quartz.config.IailabXxlJobAutoConfiguration |
| | | com.iailab.framework.quartz.config.IailabAsyncAutoConfiguration |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-monitor</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>服务监控,提供链路追踪、日志服务、指标收集等等功能</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Spring 核心 --> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-aop</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Web 相关 --> |
| | | <dependency> |
| | | <groupId>org.springframework</groupId> |
| | | <artifactId>spring-web</artifactId> |
| | | <scope>provided</scope> <!-- 设置为 provided,只有 TraceFilter 使用 --> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>jakarta.servlet</groupId> |
| | | <artifactId>jakarta.servlet-api</artifactId> |
| | | <scope>provided</scope> <!-- 设置为 provided,只有 TraceFilter 使用 --> |
| | | </dependency> |
| | | |
| | | <!-- 监控相关 --> |
| | | <dependency> |
| | | <groupId>io.opentracing</groupId> |
| | | <artifactId>opentracing-util</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.apache.skywalking</groupId> |
| | | <artifactId>apm-toolkit-trace</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.apache.skywalking</groupId> |
| | | <artifactId>apm-toolkit-logback-1.x</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.apache.skywalking</groupId> |
| | | <artifactId>apm-toolkit-opentracing</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Micrometer 对 Prometheus 的支持 --> |
| | | <dependency> |
| | | <groupId>io.micrometer</groupId> |
| | | <artifactId>micrometer-registry-prometheus</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>de.codecentric</groupId> |
| | | <artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 --> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.tracer.config; |
| | | |
| | | import io.micrometer.core.instrument.MeterRegistry; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | |
| | | /** |
| | | * Metrics 配置类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @ConditionalOnClass({MeterRegistryCustomizer.class}) |
| | | @ConditionalOnProperty(prefix = "iailab.metrics", value = "enable", matchIfMissing = true) // 允许使用 iailab.metrics.enable=false 禁用 Metrics |
| | | public class IailabMetricsAutoConfiguration { |
| | | |
| | | @Bean |
| | | public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags( |
| | | @Value("${spring.application.name}") String applicationName) { |
| | | return registry -> registry.config().commonTags("application", applicationName); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tracer.config; |
| | | |
| | | import com.iailab.framework.common.enums.WebFilterOrderEnum; |
| | | import com.iailab.framework.tracer.core.aop.BizTraceAspect; |
| | | import com.iailab.framework.tracer.core.filter.TraceFilter; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | /** |
| | | * Tracer 配置类 |
| | | * |
| | | * @author mashu |
| | | */ |
| | | @AutoConfiguration |
| | | @ConditionalOnClass({BizTraceAspect.class}) |
| | | @EnableConfigurationProperties(TracerProperties.class) |
| | | @ConditionalOnProperty(prefix = "iailab.tracer", value = "enable", matchIfMissing = true) |
| | | public class IailabTracerAutoConfiguration { |
| | | |
| | | // TODO @iailab:重要。目前 opentracing 版本存在冲突,要么保证 skywalking,要么保证阿里云短信 sdk |
| | | // @Bean |
| | | // public TracerProperties bizTracerProperties() { |
| | | // return new TracerProperties(); |
| | | // } |
| | | // |
| | | // @Bean |
| | | // public BizTraceAspect bizTracingAop() { |
| | | // return new BizTraceAspect(tracer()); |
| | | // } |
| | | // |
| | | // @Bean |
| | | // public Tracer tracer() { |
| | | // // 创建 SkywalkingTracer 对象 |
| | | // SkywalkingTracer tracer = new SkywalkingTracer(); |
| | | // // 设置为 GlobalTracer 的追踪器 |
| | | // GlobalTracer.register(tracer); |
| | | // return tracer; |
| | | // } |
| | | |
| | | /** |
| | | * 创建 TraceFilter 过滤器,响应 header 设置 traceId |
| | | */ |
| | | @Bean |
| | | public FilterRegistrationBean<TraceFilter> traceFilter() { |
| | | FilterRegistrationBean<TraceFilter> registrationBean = new FilterRegistrationBean<>(); |
| | | registrationBean.setFilter(new TraceFilter()); |
| | | registrationBean.setOrder(WebFilterOrderEnum.TRACE_FILTER); |
| | | return registrationBean; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tracer.config; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | |
| | | /** |
| | | * BizTracer配置类 |
| | | * |
| | | * @author 麻薯 |
| | | */ |
| | | @ConfigurationProperties("iailab.tracer") |
| | | @Data |
| | | public class TracerProperties { |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tracer.core.annotation; |
| | | |
| | | import java.lang.annotation.*; |
| | | |
| | | /** |
| | | * 打印业务编号 / 业务类型注解 |
| | | * |
| | | * 使用时,需要设置 SkyWalking OAP Server 的 application.yaml 配置文件,修改 SW_SEARCHABLE_TAG_KEYS 配置项, |
| | | * 增加 biz.type 和 biz.id 两值,然后重启 SkyWalking OAP Server 服务器。 |
| | | * |
| | | * @author 麻薯 |
| | | */ |
| | | @Target({ElementType.METHOD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Inherited |
| | | public @interface BizTrace { |
| | | |
| | | /** |
| | | * 业务编号 tag 名 |
| | | */ |
| | | String ID_TAG = "biz.id"; |
| | | /** |
| | | * 业务类型 tag 名 |
| | | */ |
| | | String TYPE_TAG = "biz.type"; |
| | | |
| | | /** |
| | | * @return 操作名 |
| | | */ |
| | | String operationName() default ""; |
| | | |
| | | /** |
| | | * @return 业务编号 |
| | | */ |
| | | String id(); |
| | | |
| | | /** |
| | | * @return 业务类型 |
| | | */ |
| | | String type(); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tracer.core.aop; |
| | | |
| | | import cn.hutool.core.map.MapUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.tracer.core.annotation.BizTrace; |
| | | import com.iailab.framework.common.util.spring.SpringExpressionUtils; |
| | | import com.iailab.framework.tracer.core.util.TracerFrameworkUtils; |
| | | import io.opentracing.Span; |
| | | import io.opentracing.Tracer; |
| | | import io.opentracing.tag.Tags; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.aspectj.lang.ProceedingJoinPoint; |
| | | import org.aspectj.lang.annotation.Around; |
| | | import org.aspectj.lang.annotation.Aspect; |
| | | |
| | | import java.util.Map; |
| | | |
| | | import static java.util.Arrays.asList; |
| | | |
| | | /** |
| | | * {@link BizTrace} 切面,记录业务链路 |
| | | * |
| | | * @author mashu |
| | | */ |
| | | @Aspect |
| | | @AllArgsConstructor |
| | | @Slf4j |
| | | public class BizTraceAspect { |
| | | |
| | | private static final String BIZ_OPERATION_NAME_PREFIX = "Biz/"; |
| | | |
| | | private final Tracer tracer; |
| | | |
| | | @Around(value = "@annotation(trace)") |
| | | public Object around(ProceedingJoinPoint joinPoint, BizTrace trace) throws Throwable { |
| | | // 创建 span |
| | | String operationName = getOperationName(joinPoint, trace); |
| | | Span span = tracer.buildSpan(operationName) |
| | | .withTag(Tags.COMPONENT.getKey(), "biz") |
| | | .start(); |
| | | try { |
| | | // 执行原有方法 |
| | | return joinPoint.proceed(); |
| | | } catch (Throwable throwable) { |
| | | TracerFrameworkUtils.onError(throwable, span); |
| | | throw throwable; |
| | | } finally { |
| | | // 设置 Span 的 biz 属性 |
| | | setBizTag(span, joinPoint, trace); |
| | | // 完成 Span |
| | | span.finish(); |
| | | } |
| | | } |
| | | |
| | | private String getOperationName(ProceedingJoinPoint joinPoint, BizTrace trace) { |
| | | // 自定义操作名 |
| | | if (StrUtil.isNotEmpty(trace.operationName())) { |
| | | return BIZ_OPERATION_NAME_PREFIX + trace.operationName(); |
| | | } |
| | | // 默认操作名,使用方法名 |
| | | return BIZ_OPERATION_NAME_PREFIX |
| | | + joinPoint.getSignature().getDeclaringType().getSimpleName() |
| | | + "/" + joinPoint.getSignature().getName(); |
| | | } |
| | | |
| | | private void setBizTag(Span span, ProceedingJoinPoint joinPoint, BizTrace trace) { |
| | | try { |
| | | Map<String, Object> result = SpringExpressionUtils.parseExpressions(joinPoint, asList(trace.type(), trace.id())); |
| | | span.setTag(BizTrace.TYPE_TAG, MapUtil.getStr(result, trace.type())); |
| | | span.setTag(BizTrace.ID_TAG, MapUtil.getStr(result, trace.id())); |
| | | } catch (Exception ex) { |
| | | log.error("[setBizTag][解析 bizType 与 bizId 发生异常]", ex); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tracer.core.filter; |
| | | |
| | | import com.iailab.framework.common.util.monitor.TracerUtils; |
| | | import org.springframework.web.filter.OncePerRequestFilter; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | |
| | | /** |
| | | * Trace 过滤器,打印 traceId 到 header 中返回 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TraceFilter extends OncePerRequestFilter { |
| | | |
| | | /** |
| | | * Header 名 - 链路追踪编号 |
| | | */ |
| | | private static final String HEADER_NAME_TRACE_ID = "trace-id"; |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
| | | throws IOException, ServletException { |
| | | // 设置响应 traceId |
| | | response.addHeader(HEADER_NAME_TRACE_ID, TracerUtils.getTraceId()); |
| | | // 继续过滤 |
| | | chain.doFilter(request, response); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.tracer.core.util; |
| | | |
| | | import io.opentracing.Span; |
| | | import io.opentracing.tag.Tags; |
| | | |
| | | import java.io.PrintWriter; |
| | | import java.io.StringWriter; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 链路追踪 Util |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TracerFrameworkUtils { |
| | | |
| | | /** |
| | | * 将异常记录到 Span 中,参考自 com.aliyuncs.utils.TraceUtils |
| | | * |
| | | * @param throwable 异常 |
| | | * @param span Span |
| | | */ |
| | | public static void onError(Throwable throwable, Span span) { |
| | | Tags.ERROR.set(span, Boolean.TRUE); |
| | | if (throwable != null) { |
| | | span.log(errorLogs(throwable)); |
| | | } |
| | | } |
| | | |
| | | private static Map<String, Object> errorLogs(Throwable throwable) { |
| | | Map<String, Object> errorLogs = new HashMap<String, Object>(10); |
| | | errorLogs.put("event", Tags.ERROR.getKey()); |
| | | errorLogs.put("error.object", throwable); |
| | | errorLogs.put("error.kind", throwable.getClass().getName()); |
| | | String message = throwable.getCause() != null ? throwable.getCause().getMessage() : throwable.getMessage(); |
| | | if (message != null) { |
| | | errorLogs.put("message", message); |
| | | } |
| | | StringWriter sw = new StringWriter(); |
| | | throwable.printStackTrace(new PrintWriter(sw)); |
| | | errorLogs.put("stack", sw.toString()); |
| | | return errorLogs; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 使用 SkyWalking 组件,作为链路追踪、日志中心。 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | package com.iailab.framework.tracer; |
对比新文件 |
| | |
| | | com.iailab.framework.tracer.config.IailabTracerAutoConfiguration |
| | | com.iailab.framework.tracer.config.IailabMetricsAutoConfiguration |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-mq</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <!-- DB 相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-redis</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- 消息队列相关 --> |
| | | <dependency> |
| | | <groupId>org.springframework.kafka</groupId> |
| | | <artifactId>spring-kafka</artifactId> |
| | | <!-- <optional>true</optional>--> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.amqp</groupId> |
| | | <artifactId>spring-rabbit</artifactId> |
| | | <!-- <optional>true</optional>--> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.apache.rocketmq</groupId> |
| | | <artifactId>rocketmq-spring-boot-starter</artifactId> |
| | | <!-- <optional>true</optional>--> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.common; |
| | | |
| | | /** |
| | | * |
| | | * |
| | | * @author PanZhibao |
| | | * @Description |
| | | * @createTime 2024年11月05日 |
| | | */ |
| | | public interface RoutingConstant { |
| | | |
| | | String Iailab_Data_PointCollectFinish = "Iailab.Data.PointCollectFinish"; |
| | | |
| | | // 摄像头通配路由 |
| | | String Iailab_Data_Image = "Iailab.Data.Image.*"; |
| | | |
| | | // 大华摄像头路由 |
| | | String Iailab_Data_Image_Dahua = "Iailab.Data.Image.Dahua"; |
| | | |
| | | // 海康摄像头路由 |
| | | String Iailab_Data_Image_Hikvision = "Iailab.Data.Image.Hikvision"; |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种 |
| | | */ |
| | | package com.iailab.framework.mq; |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.rabbitmq.config; |
| | | |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; |
| | | import org.springframework.amqp.support.converter.MessageConverter; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | /** |
| | | * RabbitMQ 消息队列配置类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @Slf4j |
| | | @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") |
| | | public class IailabRabbitMQAutoConfiguration { |
| | | |
| | | /** |
| | | * Jackson2JsonMessageConverter Bean:使用 jackson 序列化消息 |
| | | */ |
| | | @Bean |
| | | public MessageConverter createMessageConverter() { |
| | | return new Jackson2JsonMessageConverter(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 占位符,无特殊逻辑 |
| | | */ |
| | | package com.iailab.framework.mq.rabbitmq.core; |
对比新文件 |
| | |
| | | /** |
| | | * 消息队列,基于 RabbitMQ 提供 |
| | | */ |
| | | package com.iailab.framework.mq.rabbitmq; |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.redis.config; |
| | | |
| | | import cn.hutool.core.map.MapUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.system.SystemUtil; |
| | | import com.iailab.framework.common.enums.DocumentEnum; |
| | | import com.iailab.framework.mq.redis.core.RedisMQTemplate; |
| | | import com.iailab.framework.mq.redis.core.job.RedisPendingMessageResendJob; |
| | | import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; |
| | | import com.iailab.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; |
| | | import com.iailab.framework.redis.config.IailabRedisAutoConfiguration; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.redisson.api.RedissonClient; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.data.redis.connection.RedisServerCommands; |
| | | import org.springframework.data.redis.connection.stream.Consumer; |
| | | import org.springframework.data.redis.connection.stream.ObjectRecord; |
| | | import org.springframework.data.redis.connection.stream.ReadOffset; |
| | | import org.springframework.data.redis.connection.stream.StreamOffset; |
| | | import org.springframework.data.redis.core.RedisCallback; |
| | | import org.springframework.data.redis.core.RedisTemplate; |
| | | import org.springframework.data.redis.listener.ChannelTopic; |
| | | import org.springframework.data.redis.listener.RedisMessageListenerContainer; |
| | | import org.springframework.data.redis.stream.StreamMessageListenerContainer; |
| | | import org.springframework.scheduling.annotation.EnableScheduling; |
| | | |
| | | import java.util.List; |
| | | import java.util.Properties; |
| | | |
| | | /** |
| | | * Redis 消息队列 Consumer 配置类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | @EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息 |
| | | @AutoConfiguration(after = IailabRedisAutoConfiguration.class) |
| | | public class IailabRedisMQConsumerAutoConfiguration { |
| | | |
| | | /** |
| | | * 创建 Redis Pub/Sub 广播消费的容器 |
| | | */ |
| | | @Bean |
| | | @ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听 |
| | | public RedisMessageListenerContainer redisMessageListenerContainer( |
| | | RedisMQTemplate redisMQTemplate, List<AbstractRedisChannelMessageListener<?>> listeners) { |
| | | // 创建 RedisMessageListenerContainer 对象 |
| | | RedisMessageListenerContainer container = new RedisMessageListenerContainer(); |
| | | // 设置 RedisConnection 工厂。 |
| | | container.setConnectionFactory(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory()); |
| | | // 添加监听器 |
| | | listeners.forEach(listener -> { |
| | | listener.setRedisMQTemplate(redisMQTemplate); |
| | | container.addMessageListener(listener, new ChannelTopic(listener.getChannel())); |
| | | log.info("[redisMessageListenerContainer][注册 Channel({}) 对应的监听器({})]", |
| | | listener.getChannel(), listener.getClass().getName()); |
| | | }); |
| | | return container; |
| | | } |
| | | |
| | | /** |
| | | * 创建 Redis Stream 重新消费的任务 |
| | | */ |
| | | @Bean |
| | | @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听 |
| | | public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners, |
| | | RedisMQTemplate redisTemplate, |
| | | @Value("${spring.application.name}") String groupName, |
| | | RedissonClient redissonClient) { |
| | | return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient); |
| | | } |
| | | |
| | | /** |
| | | * 创建 Redis Stream 集群消费的容器 |
| | | * |
| | | * 基础知识:<a href="https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html">Redis Stream 的 xreadgroup 命令</a> |
| | | */ |
| | | @Bean(initMethod = "start", destroyMethod = "stop") |
| | | @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听 |
| | | public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer( |
| | | RedisMQTemplate redisMQTemplate, List<AbstractRedisStreamMessageListener<?>> listeners) { |
| | | RedisTemplate<String, ?> redisTemplate = redisMQTemplate.getRedisTemplate(); |
| | | checkRedisVersion(redisTemplate); |
| | | // 第一步,创建 StreamMessageListenerContainer 容器 |
| | | // 创建 options 配置 |
| | | StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions = |
| | | StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder() |
| | | .batchSize(10) // 一次性最多拉取多少条消息 |
| | | .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化 |
| | | .build(); |
| | | // 创建 container 对象 |
| | | StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = |
| | | StreamMessageListenerContainer.create(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory(), containerOptions); |
| | | |
| | | // 第二步,注册监听器,消费对应的 Stream 主题 |
| | | String consumerName = buildConsumerName(); |
| | | listeners.parallelStream().forEach(listener -> { |
| | | log.info("[redisStreamMessageListenerContainer][开始注册 StreamKey({}) 对应的监听器({})]", |
| | | listener.getStreamKey(), listener.getClass().getName()); |
| | | // 创建 listener 对应的消费者分组 |
| | | try { |
| | | redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup()); |
| | | } catch (Exception ignore) { |
| | | } |
| | | // 设置 listener 对应的 redisTemplate |
| | | listener.setRedisMQTemplate(redisMQTemplate); |
| | | // 创建 Consumer 对象 |
| | | Consumer consumer = Consumer.from(listener.getGroup(), consumerName); |
| | | // 设置 Consumer 消费进度,以最小消费进度为准 |
| | | StreamOffset<String> streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed()); |
| | | // 设置 Consumer 监听 |
| | | StreamMessageListenerContainer.StreamReadRequestBuilder<String> builder = StreamMessageListenerContainer.StreamReadRequest |
| | | .builder(streamOffset).consumer(consumer) |
| | | .autoAcknowledge(false) // 不自动 ack |
| | | .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false |
| | | container.register(builder.build(), listener); |
| | | log.info("[redisStreamMessageListenerContainer][完成注册 StreamKey({}) 对应的监听器({})]", |
| | | listener.getStreamKey(), listener.getClass().getName()); |
| | | }); |
| | | return container; |
| | | } |
| | | |
| | | /** |
| | | * 构建消费者名字,使用本地 IP + 进程编号的方式。 |
| | | * 参考自 RocketMQ clientId 的实现 |
| | | * |
| | | * @return 消费者名字 |
| | | */ |
| | | private static String buildConsumerName() { |
| | | return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); |
| | | } |
| | | |
| | | /** |
| | | * 校验 Redis 版本号,是否满足最低的版本号要求! |
| | | */ |
| | | private static void checkRedisVersion(RedisTemplate<String, ?> redisTemplate) { |
| | | // 获得 Redis 版本 |
| | | Properties info = redisTemplate.execute((RedisCallback<Properties>) RedisServerCommands::info); |
| | | String version = MapUtil.getStr(info, "redis_version"); |
| | | // 校验最低版本必须大于等于 5.0.0 |
| | | int majorVersion = Integer.parseInt(StrUtil.subBefore(version, '.', false)); |
| | | if (majorVersion < 5) { |
| | | throw new IllegalStateException(StrUtil.format("您当前的 Redis 版本为 {},小于最低要求的 5.0.0 版本!" + |
| | | "请参考 {} 文档进行安装。", version, DocumentEnum.REDIS_INSTALL.getUrl())); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.redis.config; |
| | | |
| | | import com.iailab.framework.mq.redis.core.RedisMQTemplate; |
| | | import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor; |
| | | import com.iailab.framework.redis.config.IailabRedisAutoConfiguration; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.data.redis.core.StringRedisTemplate; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * Redis 消息队列 Producer 配置类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | @AutoConfiguration(after = IailabRedisAutoConfiguration.class) |
| | | public class IailabRedisMQProducerAutoConfiguration { |
| | | |
| | | @Bean |
| | | public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate, |
| | | List<RedisMessageInterceptor> interceptors) { |
| | | RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate); |
| | | // 添加拦截器 |
| | | interceptors.forEach(redisMQTemplate::addInterceptor); |
| | | return redisMQTemplate; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.redis.core; |
| | | |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor; |
| | | import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage; |
| | | import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage; |
| | | import com.iailab.framework.mq.redis.core.stream.AbstractRedisStreamMessage; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Getter; |
| | | import org.springframework.data.redis.connection.stream.RecordId; |
| | | import org.springframework.data.redis.connection.stream.StreamRecords; |
| | | import org.springframework.data.redis.core.RedisTemplate; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * Redis MQ 操作模板类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AllArgsConstructor |
| | | public class RedisMQTemplate { |
| | | |
| | | @Getter |
| | | private final RedisTemplate<String, ?> redisTemplate; |
| | | /** |
| | | * 拦截器数组 |
| | | */ |
| | | @Getter |
| | | private final List<RedisMessageInterceptor> interceptors = new ArrayList<>(); |
| | | |
| | | /** |
| | | * 发送 Redis 消息,基于 Redis pub/sub 实现 |
| | | * |
| | | * @param message 消息 |
| | | */ |
| | | public <T extends AbstractRedisChannelMessage> void send(T message) { |
| | | try { |
| | | sendMessageBefore(message); |
| | | // 发送消息 |
| | | redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message)); |
| | | } finally { |
| | | sendMessageAfter(message); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 发送 Redis 消息,基于 Redis Stream 实现 |
| | | * |
| | | * @param message 消息 |
| | | * @return 消息记录的编号对象 |
| | | */ |
| | | public <T extends AbstractRedisStreamMessage> RecordId send(T message) { |
| | | try { |
| | | sendMessageBefore(message); |
| | | // 发送消息 |
| | | return redisTemplate.opsForStream().add(StreamRecords.newRecord() |
| | | .ofObject(JsonUtils.toJsonString(message)) // 设置内容 |
| | | .withStreamKey(message.getStreamKey())); // 设置 stream key |
| | | } finally { |
| | | sendMessageAfter(message); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 添加拦截器 |
| | | * |
| | | * @param interceptor 拦截器 |
| | | */ |
| | | public void addInterceptor(RedisMessageInterceptor interceptor) { |
| | | interceptors.add(interceptor); |
| | | } |
| | | |
| | | private void sendMessageBefore(AbstractRedisMessage message) { |
| | | // 正序 |
| | | interceptors.forEach(interceptor -> interceptor.sendMessageBefore(message)); |
| | | } |
| | | |
| | | private void sendMessageAfter(AbstractRedisMessage message) { |
| | | // 倒序 |
| | | for (int i = interceptors.size() - 1; i >= 0; i--) { |
| | | interceptors.get(i).sendMessageAfter(message); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.redis.core.interceptor; |
| | | |
| | | import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage; |
| | | |
| | | /** |
| | | * {@link AbstractRedisMessage} 消息拦截器 |
| | | * 通过拦截器,作为插件机制,实现拓展。 |
| | | * 例如说,多租户场景下的 MQ 消息处理 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface RedisMessageInterceptor { |
| | | |
| | | default void sendMessageBefore(AbstractRedisMessage message) { |
| | | } |
| | | |
| | | default void sendMessageAfter(AbstractRedisMessage message) { |
| | | } |
| | | |
| | | default void consumeMessageBefore(AbstractRedisMessage message) { |
| | | } |
| | | |
| | | default void consumeMessageAfter(AbstractRedisMessage message) { |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.redis.core.job; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.iailab.framework.mq.redis.core.RedisMQTemplate; |
| | | import com.iailab.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.redisson.api.RLock; |
| | | import org.redisson.api.RedissonClient; |
| | | import org.springframework.data.domain.Range; |
| | | import org.springframework.data.redis.connection.stream.*; |
| | | import org.springframework.data.redis.core.StreamOperations; |
| | | import org.springframework.scheduling.annotation.Scheduled; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | |
| | | /** |
| | | * 这个任务用于处理,crash 之后的消费者未消费完的消息 |
| | | */ |
| | | @Slf4j |
| | | @AllArgsConstructor |
| | | public class RedisPendingMessageResendJob { |
| | | |
| | | private static final String LOCK_KEY = "redis:pending:msg:lock"; |
| | | |
| | | /** |
| | | * 消息超时时间,默认 5 分钟 |
| | | * |
| | | * 1. 超时的消息才会被重新投递 |
| | | * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息5分钟过期后,再等 1 分钟才会被扫瞄到 |
| | | */ |
| | | private static final int EXPIRE_TIME = 5 * 60; |
| | | |
| | | private final List<AbstractRedisStreamMessageListener<?>> listeners; |
| | | private final RedisMQTemplate redisTemplate; |
| | | private final String groupName; |
| | | private final RedissonClient redissonClient; |
| | | |
| | | /** |
| | | * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题 |
| | | */ |
| | | @Scheduled(cron = "35 * * * * ?") |
| | | public void messageResend() { |
| | | RLock lock = redissonClient.getLock(LOCK_KEY); |
| | | // 尝试加锁 |
| | | if (lock.tryLock()) { |
| | | try { |
| | | execute(); |
| | | } catch (Exception ex) { |
| | | log.error("[messageResend][执行异常]", ex); |
| | | } finally { |
| | | lock.unlock(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 执行清理逻辑 |
| | | * |
| | | * @see <a href="https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/480/files">讨论</a> |
| | | */ |
| | | private void execute() { |
| | | StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream(); |
| | | listeners.forEach(listener -> { |
| | | PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), groupName)); |
| | | // 每个消费者的 pending 队列消息数量 |
| | | Map<String, Long> pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer(); |
| | | pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> { |
| | | log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount); |
| | | // 每个消费者的 pending消息的详情信息 |
| | | PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(groupName, consumerName), Range.unbounded(), pendingMessageCount); |
| | | if (pendingMessages.isEmpty()) { |
| | | return; |
| | | } |
| | | pendingMessages.forEach(pendingMessage -> { |
| | | // 获取消息上一次传递到 consumer 的时间, |
| | | long lastDelivery = pendingMessage.getElapsedTimeSinceLastDelivery().getSeconds(); |
| | | if (lastDelivery < EXPIRE_TIME){ |
| | | return; |
| | | } |
| | | // 获取指定 id 的消息体 |
| | | List<MapRecord<String, Object, Object>> records = ops.range(listener.getStreamKey(), |
| | | Range.of(Range.Bound.inclusive(pendingMessage.getIdAsString()), Range.Bound.inclusive(pendingMessage.getIdAsString()))); |
| | | if (CollUtil.isEmpty(records)) { |
| | | return; |
| | | } |
| | | // 重新投递消息 |
| | | redisTemplate.getRedisTemplate().opsForStream().add(StreamRecords.newRecord() |
| | | .ofObject(records.get(0).getValue()) // 设置内容 |
| | | .withStreamKey(listener.getStreamKey())); |
| | | // ack 消息消费完成 |
| | | redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, records.get(0)); |
| | | log.info("[processPendingMessage][消息({})重新投递成功]", records.get(0).getId()); |
| | | }); |
| | | }); |
| | | }); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.redis.core.message; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * Redis 消息抽象基类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Data |
| | | public abstract class AbstractRedisMessage { |
| | | |
| | | /** |
| | | * 头 |
| | | */ |
| | | private Map<String, String> headers = new HashMap<>(); |
| | | |
| | | public String getHeader(String key) { |
| | | return headers.get(key); |
| | | } |
| | | |
| | | public void addHeader(String key, String value) { |
| | | headers.put(key, value); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.redis.core.pubsub; |
| | | |
| | | import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage; |
| | | import com.fasterxml.jackson.annotation.JsonIgnore; |
| | | |
| | | /** |
| | | * Redis Channel Message 抽象类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public abstract class AbstractRedisChannelMessage extends AbstractRedisMessage { |
| | | |
| | | /** |
| | | * 获得 Redis Channel,默认使用类名 |
| | | * |
| | | * @return Channel |
| | | */ |
| | | @JsonIgnore // 避免序列化。原因是,Redis 发布 Channel 消息的时候,已经会指定。 |
| | | public String getChannel() { |
| | | return getClass().getSimpleName(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.redis.core.pubsub; |
| | | |
| | | import cn.hutool.core.util.TypeUtil; |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.mq.redis.core.RedisMQTemplate; |
| | | import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor; |
| | | import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage; |
| | | import lombok.Setter; |
| | | import lombok.SneakyThrows; |
| | | import org.springframework.data.redis.connection.Message; |
| | | import org.springframework.data.redis.connection.MessageListener; |
| | | |
| | | import java.lang.reflect.Type; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * Redis Pub/Sub 监听器抽象类,用于实现广播消费 |
| | | * |
| | | * @param <T> 消息类型。一定要填写噢,不然会报错 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public abstract class AbstractRedisChannelMessageListener<T extends AbstractRedisChannelMessage> implements MessageListener { |
| | | |
| | | /** |
| | | * 消息类型 |
| | | */ |
| | | private final Class<T> messageType; |
| | | /** |
| | | * Redis Channel |
| | | */ |
| | | private final String channel; |
| | | /** |
| | | * RedisMQTemplate |
| | | */ |
| | | @Setter |
| | | private RedisMQTemplate redisMQTemplate; |
| | | |
| | | @SneakyThrows |
| | | protected AbstractRedisChannelMessageListener() { |
| | | this.messageType = getMessageClass(); |
| | | this.channel = messageType.getDeclaredConstructor().newInstance().getChannel(); |
| | | } |
| | | |
| | | /** |
| | | * 获得 Sub 订阅的 Redis Channel 通道 |
| | | * |
| | | * @return channel |
| | | */ |
| | | public final String getChannel() { |
| | | return channel; |
| | | } |
| | | |
| | | @Override |
| | | public final void onMessage(Message message, byte[] bytes) { |
| | | T messageObj = JsonUtils.parseObject(message.getBody(), messageType); |
| | | try { |
| | | consumeMessageBefore(messageObj); |
| | | // 消费消息 |
| | | this.onMessage(messageObj); |
| | | } finally { |
| | | consumeMessageAfter(messageObj); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 处理消息 |
| | | * |
| | | * @param message 消息 |
| | | */ |
| | | public abstract void onMessage(T message); |
| | | |
| | | /** |
| | | * 通过解析类上的泛型,获得消息类型 |
| | | * |
| | | * @return 消息类型 |
| | | */ |
| | | @SuppressWarnings("unchecked") |
| | | private Class<T> getMessageClass() { |
| | | Type type = TypeUtil.getTypeArgument(getClass(), 0); |
| | | if (type == null) { |
| | | throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); |
| | | } |
| | | return (Class<T>) type; |
| | | } |
| | | |
| | | private void consumeMessageBefore(AbstractRedisMessage message) { |
| | | assert redisMQTemplate != null; |
| | | List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors(); |
| | | // 正序 |
| | | interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message)); |
| | | } |
| | | |
| | | private void consumeMessageAfter(AbstractRedisMessage message) { |
| | | assert redisMQTemplate != null; |
| | | List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors(); |
| | | // 倒序 |
| | | for (int i = interceptors.size() - 1; i >= 0; i--) { |
| | | interceptors.get(i).consumeMessageAfter(message); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.redis.core.stream; |
| | | |
| | | import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage; |
| | | import com.fasterxml.jackson.annotation.JsonIgnore; |
| | | |
| | | /** |
| | | * Redis Stream Message 抽象类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public abstract class AbstractRedisStreamMessage extends AbstractRedisMessage { |
| | | |
| | | /** |
| | | * 获得 Redis Stream Key,默认使用类名 |
| | | * |
| | | * @return Channel |
| | | */ |
| | | @JsonIgnore // 避免序列化 |
| | | public String getStreamKey() { |
| | | return getClass().getSimpleName(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mq.redis.core.stream; |
| | | |
| | | import cn.hutool.core.util.TypeUtil; |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.mq.redis.core.RedisMQTemplate; |
| | | import com.iailab.framework.mq.redis.core.interceptor.RedisMessageInterceptor; |
| | | import com.iailab.framework.mq.redis.core.message.AbstractRedisMessage; |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | import lombok.SneakyThrows; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.data.redis.connection.stream.ObjectRecord; |
| | | import org.springframework.data.redis.stream.StreamListener; |
| | | |
| | | import java.lang.reflect.Type; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * Redis Stream 监听器抽象类,用于实现集群消费 |
| | | * |
| | | * @param <T> 消息类型。一定要填写噢,不然会报错 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public abstract class AbstractRedisStreamMessageListener<T extends AbstractRedisStreamMessage> |
| | | implements StreamListener<String, ObjectRecord<String, String>> { |
| | | |
| | | /** |
| | | * 消息类型 |
| | | */ |
| | | private final Class<T> messageType; |
| | | /** |
| | | * Redis Channel |
| | | */ |
| | | @Getter |
| | | private final String streamKey; |
| | | |
| | | /** |
| | | * Redis 消费者分组,默认使用 spring.application.name 名字 |
| | | */ |
| | | @Value("${spring.application.name}") |
| | | @Getter |
| | | private String group; |
| | | /** |
| | | * RedisMQTemplate |
| | | */ |
| | | @Setter |
| | | private RedisMQTemplate redisMQTemplate; |
| | | |
| | | @SneakyThrows |
| | | protected AbstractRedisStreamMessageListener() { |
| | | this.messageType = getMessageClass(); |
| | | this.streamKey = messageType.getDeclaredConstructor().newInstance().getStreamKey(); |
| | | } |
| | | |
| | | @Override |
| | | public void onMessage(ObjectRecord<String, String> message) { |
| | | // 消费消息 |
| | | T messageObj = JsonUtils.parseObject(message.getValue(), messageType); |
| | | try { |
| | | consumeMessageBefore(messageObj); |
| | | // 消费消息 |
| | | this.onMessage(messageObj); |
| | | // ack 消息消费完成 |
| | | redisMQTemplate.getRedisTemplate().opsForStream().acknowledge(group, message); |
| | | // TODO iailab:需要额外考虑以下几个点: |
| | | // 1. 处理异常的情况 |
| | | // 2. 发送日志;以及事务的结合 |
| | | // 3. 消费日志;以及通用的幂等性 |
| | | // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638 |
| | | } finally { |
| | | consumeMessageAfter(messageObj); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 处理消息 |
| | | * |
| | | * @param message 消息 |
| | | */ |
| | | public abstract void onMessage(T message); |
| | | |
| | | /** |
| | | * 通过解析类上的泛型,获得消息类型 |
| | | * |
| | | * @return 消息类型 |
| | | */ |
| | | @SuppressWarnings("unchecked") |
| | | private Class<T> getMessageClass() { |
| | | Type type = TypeUtil.getTypeArgument(getClass(), 0); |
| | | if (type == null) { |
| | | throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); |
| | | } |
| | | return (Class<T>) type; |
| | | } |
| | | |
| | | private void consumeMessageBefore(AbstractRedisMessage message) { |
| | | assert redisMQTemplate != null; |
| | | List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors(); |
| | | // 正序 |
| | | interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message)); |
| | | } |
| | | |
| | | private void consumeMessageAfter(AbstractRedisMessage message) { |
| | | assert redisMQTemplate != null; |
| | | List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors(); |
| | | // 倒序 |
| | | for (int i = interceptors.size() - 1; i >= 0; i--) { |
| | | interceptors.get(i).consumeMessageAfter(message); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 消息队列,基于 Redis 提供: |
| | | * 1. 基于 Pub/Sub 实现广播消费 |
| | | * 2. 基于 Stream 实现集群消费 |
| | | */ |
| | | package com.iailab.framework.mq.redis; |
对比新文件 |
| | |
| | | com.iailab.framework.mq.redis.config.IailabRedisMQProducerAutoConfiguration |
| | | com.iailab.framework.mq.redis.config.IailabRedisMQConsumerAutoConfiguration |
| | | com.iailab.framework.mq.rabbitmq.config.IailabRabbitMQAutoConfiguration |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-mybatis</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>数据库连接池、多数据源、事务、MyBatis 拓展</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Web 相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-web</artifactId> |
| | | <scope>provided</scope> <!-- 设置为 provided,只有 OncePerRequestFilter 使用到 --> |
| | | </dependency> |
| | | |
| | | <!-- DB 相关 --> |
| | | <dependency> |
| | | <groupId>com.mysql</groupId> |
| | | <artifactId>mysql-connector-j</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.oracle.database.jdbc</groupId> |
| | | <artifactId>ojdbc8</artifactId> |
| | | <!-- <optional>true</optional>--> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.postgresql</groupId> |
| | | <artifactId>postgresql</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.microsoft.sqlserver</groupId> |
| | | <artifactId>mssql-jdbc</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.dameng</groupId> |
| | | <artifactId>DmJdbcDriver18</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.alibaba</groupId> |
| | | <artifactId>druid-spring-boot-starter</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.baomidou</groupId> |
| | | <artifactId>mybatis-plus-boot-starter</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.baomidou</groupId> |
| | | <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 --> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.github.yulichang</groupId> |
| | | <artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 --> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 --> |
| | | <artifactId>easy-trans-spring-boot-starter</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.fhs-opensource</groupId> |
| | | <artifactId>easy-trans-mybatis-plus-extend</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.testng</groupId> |
| | | <artifactId>testng</artifactId> |
| | | <version>RELEASE</version> |
| | | <scope>compile</scope> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.alibaba.nacos</groupId> |
| | | <artifactId>nacos-client</artifactId> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * |
| | | * https://www.renren.io |
| | | * |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.common.dao; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | |
| | | /** |
| | | * 基础Dao |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | * @since 1.0.0 |
| | | */ |
| | | public interface BaseDao<T> extends BaseMapper<T> { |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * |
| | | * https://www.renren.io |
| | | * |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.common.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.FieldFill; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Date; |
| | | |
| | | /** |
| | | * 基础实体类,所有实体都需要继承 |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | */ |
| | | @Data |
| | | public abstract class BaseEntity implements Serializable { |
| | | /** |
| | | * id |
| | | */ |
| | | @TableId |
| | | private Long id; |
| | | /** |
| | | * 创建者 |
| | | */ |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private Long creator; |
| | | /** |
| | | * 创建时间 |
| | | */ |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private Date createDate; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common; |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * |
| | | * https://www.renren.io |
| | | * |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.common.page; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import io.swagger.v3.oas.annotations.tags.Tag; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 分页工具类 |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | */ |
| | | @Data |
| | | @Tag(name = "分页数据") |
| | | public class PageData<T> implements Serializable { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | @Schema(description = "总记录数") |
| | | private int total; |
| | | |
| | | @Schema(description = "列表数据") |
| | | private List<T> list; |
| | | |
| | | /** |
| | | * 分页 |
| | | * @param list 列表数据 |
| | | * @param total 总记录数 |
| | | */ |
| | | public PageData(List<T> list, long total) { |
| | | this.list = list; |
| | | this.total = (int)total; |
| | | } |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * |
| | | * https://www.renren.io |
| | | * |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.common.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.Wrapper; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Collection; |
| | | |
| | | /** |
| | | * 基础服务接口,所有Service接口都要继承 |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | */ |
| | | public interface BaseService<T> { |
| | | Class<T> currentModelClass(); |
| | | |
| | | /** |
| | | * <p> |
| | | * 插入一条记录(选择字段,策略插入) |
| | | * </p> |
| | | * |
| | | * @param entity 实体对象 |
| | | */ |
| | | boolean insert(T entity); |
| | | |
| | | /** |
| | | * <p> |
| | | * 插入(批量),该方法不支持 Oracle、SQL Server |
| | | * </p> |
| | | * |
| | | * @param entityList 实体对象集合 |
| | | */ |
| | | boolean insertBatch(Collection<T> entityList); |
| | | |
| | | /** |
| | | * <p> |
| | | * 插入(批量),该方法不支持 Oracle、SQL Server |
| | | * </p> |
| | | * |
| | | * @param entityList 实体对象集合 |
| | | * @param batchSize 插入批次数量 |
| | | */ |
| | | boolean insertBatch(Collection<T> entityList, int batchSize); |
| | | |
| | | /** |
| | | * <p> |
| | | * 根据 ID 选择修改 |
| | | * </p> |
| | | * |
| | | * @param entity 实体对象 |
| | | */ |
| | | boolean updateById(T entity); |
| | | |
| | | /** |
| | | * <p> |
| | | * 根据 whereEntity 条件,更新记录 |
| | | * </p> |
| | | * |
| | | * @param entity 实体对象 |
| | | * @param updateWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper} |
| | | */ |
| | | boolean update(T entity, Wrapper<T> updateWrapper); |
| | | |
| | | /** |
| | | * <p> |
| | | * 根据ID 批量更新 |
| | | * </p> |
| | | * |
| | | * @param entityList 实体对象集合 |
| | | */ |
| | | boolean updateBatchById(Collection<T> entityList); |
| | | |
| | | /** |
| | | * <p> |
| | | * 根据ID 批量更新 |
| | | * </p> |
| | | * |
| | | * @param entityList 实体对象集合 |
| | | * @param batchSize 更新批次数量 |
| | | */ |
| | | boolean updateBatchById(Collection<T> entityList, int batchSize); |
| | | |
| | | /** |
| | | * <p> |
| | | * 根据 ID 查询 |
| | | * </p> |
| | | * |
| | | * @param id 主键ID |
| | | */ |
| | | T selectById(Serializable id); |
| | | |
| | | /** |
| | | * <p> |
| | | * 根据 ID 删除 |
| | | * </p> |
| | | * |
| | | * @param id 主键ID |
| | | */ |
| | | boolean deleteById(Serializable id); |
| | | |
| | | /** |
| | | * <p> |
| | | * 删除(根据ID 批量删除) |
| | | * </p> |
| | | * |
| | | * @param idList 主键ID列表 |
| | | */ |
| | | boolean deleteBatchIds(Collection<? extends Serializable> idList); |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * <p> |
| | | * https://www.renren.io |
| | | * <p> |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.common.service; |
| | | |
| | | import com.iailab.framework.common.page.PageData; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * CRUD基础服务接口 |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | */ |
| | | public interface CrudService<T, D> extends BaseService<T> { |
| | | |
| | | PageData<D> page(Map<String, Object> params); |
| | | |
| | | List<D> list(Map<String, Object> params); |
| | | |
| | | D get(Long id); |
| | | |
| | | void save(D dto); |
| | | |
| | | void update(D dto); |
| | | |
| | | void delete(Long[] ids); |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * |
| | | * https://www.renren.io |
| | | * |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.common.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.Wrapper; |
| | | import com.baomidou.mybatisplus.core.enums.SqlMethod; |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.core.metadata.OrderItem; |
| | | import com.baomidou.mybatisplus.core.toolkit.Constants; |
| | | import com.baomidou.mybatisplus.core.toolkit.ReflectionKit; |
| | | import com.baomidou.mybatisplus.core.toolkit.StringUtils; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; |
| | | import com.iailab.framework.common.constant.Constant; |
| | | import com.iailab.framework.common.page.PageData; |
| | | import com.iailab.framework.common.service.BaseService; |
| | | import com.iailab.framework.common.util.object.BeanUtils; |
| | | import org.apache.ibatis.binding.MapperMethod; |
| | | import org.apache.ibatis.logging.Log; |
| | | import org.apache.ibatis.logging.LogFactory; |
| | | import org.apache.ibatis.session.SqlSession; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.function.BiConsumer; |
| | | |
| | | /** |
| | | * 基础服务类,所有Service都要继承 |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | */ |
| | | public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements BaseService<T> { |
| | | @Autowired |
| | | protected M baseDao; |
| | | protected Log log = LogFactory.getLog(getClass()); |
| | | |
| | | /** |
| | | * 获取分页对象 |
| | | * @param params 分页查询参数 |
| | | * @param defaultOrderField 默认排序字段 |
| | | * @param isAsc 排序方式 |
| | | */ |
| | | protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) { |
| | | //分页参数 |
| | | long curPage = 1; |
| | | long limit = 10; |
| | | |
| | | if(params.get(Constant.PAGE) != null){ |
| | | curPage = Long.parseLong((String)params.get(Constant.PAGE)); |
| | | } |
| | | if(params.get(Constant.LIMIT) != null){ |
| | | limit = Long.parseLong((String)params.get(Constant.LIMIT)); |
| | | } |
| | | |
| | | //分页对象 |
| | | Page<T> page = new Page<>(curPage, limit); |
| | | |
| | | //分页参数 |
| | | params.put(Constant.PAGE, page); |
| | | |
| | | //排序字段 |
| | | String orderField = (String)params.get(Constant.ORDER_FIELD); |
| | | String order = (String)params.get(Constant.ORDER); |
| | | |
| | | //前端字段排序 |
| | | if(StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)){ |
| | | if(Constant.ASC.equalsIgnoreCase(order)) { |
| | | return page.addOrder(OrderItem.asc(orderField)); |
| | | }else { |
| | | return page.addOrder(OrderItem.desc(orderField)); |
| | | } |
| | | } |
| | | |
| | | //没有排序字段,则不排序 |
| | | if(StringUtils.isBlank(defaultOrderField)){ |
| | | return page; |
| | | } |
| | | |
| | | //默认排序 |
| | | if(isAsc) { |
| | | page.addOrder(OrderItem.asc(defaultOrderField)); |
| | | }else { |
| | | page.addOrder(OrderItem.desc(defaultOrderField)); |
| | | } |
| | | |
| | | return page; |
| | | } |
| | | |
| | | protected <T> PageData<T> getPageData(List<?> list, long total, Class<T> target){ |
| | | List<T> targetList = BeanUtils.toBean(list, target); |
| | | |
| | | return new PageData<>(targetList, total); |
| | | } |
| | | |
| | | protected <T> PageData<T> getPageData(IPage page, Class<T> target){ |
| | | return getPageData(page.getRecords(), page.getTotal(), target); |
| | | } |
| | | |
| | | protected void paramsToLike(Map<String, Object> params, String... likes){ |
| | | for (String like : likes){ |
| | | String val = (String)params.get(like); |
| | | if (StringUtils.isNotBlank(val)){ |
| | | params.put(like, "%" + val + "%"); |
| | | }else { |
| | | params.put(like, null); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * <p> |
| | | * 判断数据库操作是否成功 |
| | | * </p> |
| | | * <p> |
| | | * 注意!! 该方法为 Integer 判断,不可传入 int 基本类型 |
| | | * </p> |
| | | * |
| | | * @param result 数据库操作返回影响条数 |
| | | * @return boolean |
| | | */ |
| | | protected static boolean retBool(Integer result) { |
| | | return SqlHelper.retBool(result); |
| | | } |
| | | |
| | | protected Class<M> currentMapperClass() { |
| | | return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0); |
| | | } |
| | | |
| | | @Override |
| | | public Class<T> currentModelClass() { |
| | | return (Class<T>)ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1); |
| | | } |
| | | |
| | | protected String getSqlStatement(SqlMethod sqlMethod) { |
| | | return SqlHelper.getSqlStatement(this.currentMapperClass(), sqlMethod); |
| | | } |
| | | |
| | | @Override |
| | | public boolean insert(T entity) { |
| | | return BaseServiceImpl.retBool(baseDao.insert(entity)); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean insertBatch(Collection<T> entityList) { |
| | | return insertBatch(entityList, 100); |
| | | } |
| | | |
| | | /** |
| | | * 批量插入 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean insertBatch(Collection<T> entityList, int batchSize) { |
| | | String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE); |
| | | return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity)); |
| | | } |
| | | |
| | | /** |
| | | * 执行批量操作 |
| | | */ |
| | | protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) { |
| | | return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer); |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public boolean updateById(T entity) { |
| | | return BaseServiceImpl.retBool(baseDao.updateById(entity)); |
| | | } |
| | | |
| | | @Override |
| | | public boolean update(T entity, Wrapper<T> updateWrapper) { |
| | | return BaseServiceImpl.retBool(baseDao.update(entity, updateWrapper)); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean updateBatchById(Collection<T> entityList) { |
| | | return updateBatchById(entityList, 30); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean updateBatchById(Collection<T> entityList, int batchSize) { |
| | | String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID); |
| | | return executeBatch(entityList, batchSize, (sqlSession, entity) -> { |
| | | MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>(); |
| | | param.put(Constants.ENTITY, entity); |
| | | sqlSession.update(sqlStatement, param); |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public T selectById(Serializable id) { |
| | | return baseDao.selectById(id); |
| | | } |
| | | |
| | | @Override |
| | | public boolean deleteById(Serializable id) { |
| | | return SqlHelper.retBool(baseDao.deleteById(id)); |
| | | } |
| | | |
| | | @Override |
| | | public boolean deleteBatchIds(Collection<? extends Serializable> idList) { |
| | | return SqlHelper.retBool(baseDao.deleteBatchIds(idList)); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * <p> |
| | | * https://www.renren.io |
| | | * <p> |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.common.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.core.toolkit.ReflectionKit; |
| | | import com.iailab.framework.common.page.PageData; |
| | | import com.iailab.framework.common.service.CrudService; |
| | | import com.iailab.framework.common.util.object.ConvertUtils; |
| | | import org.springframework.beans.BeanUtils; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * CRUD基础服务类 |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | */ |
| | | public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T> implements CrudService<T, D> { |
| | | |
| | | protected Class<D> currentDtoClass() { |
| | | return (Class<D>)ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2); |
| | | } |
| | | |
| | | @Override |
| | | public PageData<D> page(Map<String, Object> params) { |
| | | IPage<T> page = baseDao.selectPage( |
| | | getPage(params, null, false), |
| | | getWrapper(params) |
| | | ); |
| | | |
| | | return getPageData(page, currentDtoClass()); |
| | | } |
| | | |
| | | @Override |
| | | public List<D> list(Map<String, Object> params) { |
| | | List<T> entityList = baseDao.selectList(getWrapper(params)); |
| | | |
| | | return ConvertUtils.sourceToTarget(entityList, currentDtoClass()); |
| | | } |
| | | |
| | | public abstract QueryWrapper<T> getWrapper(Map<String, Object> params); |
| | | |
| | | @Override |
| | | public D get(Long id) { |
| | | T entity = baseDao.selectById(id); |
| | | |
| | | return ConvertUtils.sourceToTarget(entity, currentDtoClass()); |
| | | } |
| | | |
| | | @Override |
| | | public void save(D dto) { |
| | | T entity = ConvertUtils.sourceToTarget(dto, currentModelClass()); |
| | | insert(entity); |
| | | |
| | | //copy主键值到dto |
| | | BeanUtils.copyProperties(entity, dto); |
| | | } |
| | | |
| | | @Override |
| | | public void update(D dto) { |
| | | T entity = ConvertUtils.sourceToTarget(dto, currentModelClass()); |
| | | updateById(entity); |
| | | } |
| | | |
| | | @Override |
| | | public void delete(Long[] ids) { |
| | | baseDao.deleteBatchIds(Arrays.asList(ids)); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datasource.config; |
| | | |
| | | import com.iailab.framework.datasource.core.filter.DruidAdRemoveFilter; |
| | | import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.transaction.annotation.EnableTransactionManagement; |
| | | |
| | | /** |
| | | * 数据库配置类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理 |
| | | @EnableConfigurationProperties(DruidStatProperties.class) |
| | | public class IailabDataSourceAutoConfiguration { |
| | | |
| | | /** |
| | | * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告 |
| | | */ |
| | | @Bean |
| | | @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true") |
| | | public FilterRegistrationBean<DruidAdRemoveFilter> druidAdRemoveFilterFilter(DruidStatProperties properties) { |
| | | // 获取 druid web 监控页面的参数 |
| | | DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); |
| | | // 提取 common.js 的配置路径 |
| | | String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; |
| | | String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); |
| | | // 创建 DruidAdRemoveFilter Bean |
| | | FilterRegistrationBean<DruidAdRemoveFilter> registrationBean = new FilterRegistrationBean<>(); |
| | | registrationBean.setFilter(new DruidAdRemoveFilter()); |
| | | registrationBean.addUrlPatterns(commonJsPattern); |
| | | return registrationBean; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datasource.core.enums; |
| | | |
| | | /** |
| | | * 对应于多数据源中不同数据源配置 |
| | | * |
| | | * 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。 |
| | | * 注意,默认是 {@link #MASTER} 数据源 |
| | | * |
| | | * 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html |
| | | */ |
| | | public interface DataSourceEnum { |
| | | |
| | | /** |
| | | * 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解 |
| | | */ |
| | | String MASTER = "master"; |
| | | /** |
| | | * 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解 |
| | | */ |
| | | String SLAVE = "slave"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datasource.core.filter; |
| | | |
| | | import com.alibaba.druid.util.Utils; |
| | | import org.springframework.web.filter.OncePerRequestFilter; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | |
| | | /** |
| | | * Druid 底部广告过滤器 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DruidAdRemoveFilter extends OncePerRequestFilter { |
| | | |
| | | /** |
| | | * common.js 的路径 |
| | | */ |
| | | private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js"; |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
| | | throws ServletException, IOException { |
| | | chain.doFilter(request, response); |
| | | // 重置缓冲区,响应头不会被重置 |
| | | response.resetBuffer(); |
| | | // 获取 common.js |
| | | String text = Utils.readFromResource(COMMON_JS_ILE_PATH); |
| | | // 正则替换 banner, 除去底部的广告信息 |
| | | text = text.replaceAll("<a.*?banner\"></a><br/>", ""); |
| | | text = text.replaceAll("powered.*?shrek.wang</a>", ""); |
| | | response.getWriter().write(text); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 数据库连接池,采用 Druid |
| | | * 多数据源,采用爆米花 |
| | | */ |
| | | package com.iailab.framework.datasource; |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.config; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.mybatis.core.enums.SqlConstants; |
| | | import com.iailab.framework.mybatis.core.handler.DefaultDBFieldHandler; |
| | | import com.baomidou.mybatisplus.annotation.DbType; |
| | | import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; |
| | | import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; |
| | | import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; |
| | | import com.baomidou.mybatisplus.extension.incrementer.*; |
| | | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
| | | import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.mybatis.spring.annotation.MapperScan; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.core.env.ConfigurableEnvironment; |
| | | |
| | | /** |
| | | * MyBaits 配置类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration(before = MybatisPlusAutoConfiguration.class) // 目的:先于 MyBatis Plus 自动配置,避免 @MapperScan 可能扫描不到 Mapper 打印 warn 日志 |
| | | @MapperScan(value = "${iailab.info.base-package}", annotationClass = Mapper.class, |
| | | lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试 |
| | | public class IailabMybatisAutoConfiguration { |
| | | |
| | | @Bean |
| | | public MybatisPlusInterceptor mybatisPlusInterceptor() { |
| | | MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); |
| | | mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 |
| | | return mybatisPlusInterceptor; |
| | | } |
| | | |
| | | @Bean |
| | | public MetaObjectHandler defaultMetaObjectHandler(){ |
| | | return new DefaultDBFieldHandler(); // 自动填充参数类 |
| | | } |
| | | |
| | | @Bean |
| | | @ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT") |
| | | public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) { |
| | | DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment); |
| | | if (dbType != null) { |
| | | switch (dbType) { |
| | | case POSTGRE_SQL: |
| | | return new PostgreKeyGenerator(); |
| | | case ORACLE: |
| | | case ORACLE_12C: |
| | | return new OracleKeyGenerator(); |
| | | case H2: |
| | | return new H2KeyGenerator(); |
| | | case KINGBASE_ES: |
| | | return new KingbaseKeyGenerator(); |
| | | case DM: |
| | | return new DmKeyGenerator(); |
| | | } |
| | | } |
| | | // 找不到合适的 IKeyGenerator 实现类 |
| | | throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.config; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.util.collection.SetUtils; |
| | | import com.iailab.framework.mybatis.core.enums.SqlConstants; |
| | | import com.iailab.framework.mybatis.core.util.JdbcUtils; |
| | | import com.baomidou.mybatisplus.annotation.DbType; |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.boot.SpringApplication; |
| | | import org.springframework.boot.env.EnvironmentPostProcessor; |
| | | import org.springframework.core.env.ConfigurableEnvironment; |
| | | |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * 当 IdType 为 {@link IdType#NONE} 时,根据 PRIMARY 数据源所使用的数据库,自动设置 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor { |
| | | |
| | | private static final String ID_TYPE_KEY = "mybatis-plus.global-config.db-config.id-type"; |
| | | |
| | | private static final String DATASOURCE_DYNAMIC_KEY = "spring.datasource.dynamic"; |
| | | |
| | | private static final String QUARTZ_JOB_STORE_DRIVER_KEY = "spring.quartz.properties.org.quartz.jobStore.driverDelegateClass"; |
| | | |
| | | private static final Set<DbType> INPUT_ID_TYPES = SetUtils.asSet(DbType.ORACLE, DbType.ORACLE_12C, |
| | | DbType.POSTGRE_SQL, DbType.KINGBASE_ES, DbType.DB2, DbType.H2); |
| | | |
| | | @Override |
| | | public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { |
| | | // 如果获取不到 DbType,则不进行处理 |
| | | DbType dbType = getDbType(environment); |
| | | if (dbType == null) { |
| | | return; |
| | | } |
| | | |
| | | // 设置 Quartz JobStore 对应的 Driver |
| | | // TODO iailab:暂时没有找到特别合适的地方,先放在这里 |
| | | setJobStoreDriverIfPresent(environment, dbType); |
| | | // 初始化 SQL 静态变量 |
| | | SqlConstants.init(dbType); |
| | | // 如果非 NONE,则不进行处理 |
| | | IdType idType = getIdType(environment); |
| | | if (idType != IdType.NONE) { |
| | | return; |
| | | } |
| | | // 情况一,用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 |
| | | if (INPUT_ID_TYPES.contains(dbType)) { |
| | | setIdType(environment, IdType.INPUT); |
| | | return; |
| | | } |
| | | // 情况二,自增 ID,适合 MySQL 等直接自增的数据库 |
| | | setIdType(environment, IdType.AUTO); |
| | | } |
| | | |
| | | public IdType getIdType(ConfigurableEnvironment environment) { |
| | | return environment.getProperty(ID_TYPE_KEY, IdType.class); |
| | | } |
| | | |
| | | public void setIdType(ConfigurableEnvironment environment, IdType idType) { |
| | | environment.getSystemProperties().put(ID_TYPE_KEY, idType); |
| | | log.info("[setIdType][修改 MyBatis Plus 的 idType 为({})]", idType); |
| | | } |
| | | |
| | | public void setJobStoreDriverIfPresent(ConfigurableEnvironment environment, DbType dbType) { |
| | | String driverClass = environment.getProperty(QUARTZ_JOB_STORE_DRIVER_KEY); |
| | | if (StrUtil.isNotEmpty(driverClass)) { |
| | | return; |
| | | } |
| | | // 根据 dbType 类型,获取对应的 driverClass |
| | | switch (dbType) { |
| | | case POSTGRE_SQL: |
| | | driverClass = "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate"; |
| | | break; |
| | | case ORACLE: |
| | | case ORACLE_12C: |
| | | driverClass = "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate"; |
| | | break; |
| | | case SQL_SERVER: |
| | | case SQL_SERVER2005: |
| | | driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate"; |
| | | break; |
| | | } |
| | | // 设置 driverClass 变量 |
| | | if (StrUtil.isNotEmpty(driverClass)) { |
| | | environment.getSystemProperties().put(QUARTZ_JOB_STORE_DRIVER_KEY, driverClass); |
| | | } |
| | | } |
| | | |
| | | public static DbType getDbType(ConfigurableEnvironment environment) { |
| | | String primary = environment.getProperty(DATASOURCE_DYNAMIC_KEY + "." + "primary"); |
| | | if (StrUtil.isEmpty(primary)) { |
| | | return null; |
| | | } |
| | | String url = environment.getProperty(DATASOURCE_DYNAMIC_KEY + ".datasource." + primary + ".url"); |
| | | if (StrUtil.isEmpty(url)) { |
| | | return null; |
| | | } |
| | | return JdbcUtils.getDbType(url); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | //package com.iailab.framework.mybatis.config; |
| | | // |
| | | //import com.alibaba.fastjson.JSONArray; |
| | | //import com.alibaba.fastjson.JSONObject; |
| | | //import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
| | | //import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; |
| | | //import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; |
| | | //import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; |
| | | //import com.iailab.framework.mybatis.core.handler.MybatisHandler; |
| | | //import com.iailab.framework.mybatis.interceptor.DataFilterInterceptor; |
| | | //import org.apache.ibatis.session.SqlSessionFactory; |
| | | //import org.apache.ibatis.type.JdbcType; |
| | | //import org.mybatis.spring.SqlSessionFactoryBean; |
| | | //import org.mybatis.spring.SqlSessionTemplate; |
| | | //import org.springframework.beans.factory.annotation.Value; |
| | | //import org.springframework.boot.jdbc.DataSourceBuilder; |
| | | //import org.springframework.context.annotation.Bean; |
| | | //import org.springframework.context.annotation.Configuration; |
| | | //import org.springframework.context.annotation.Primary; |
| | | //import org.springframework.core.io.support.PathMatchingResourcePatternResolver; |
| | | //import org.springframework.jdbc.datasource.DataSourceTransactionManager; |
| | | // |
| | | // |
| | | ///** |
| | | // * @author houzhongjian |
| | | // * @Title: MyBatisConfiguration |
| | | // * @ProjectName design-parent |
| | | // * @Description: 解决独立启动某个服务时报错问题 No typehandler found for property sqlSessionTemplate |
| | | // * @date 2024/7/2 16:35 |
| | | // */ |
| | | //@Configuration |
| | | //public class MyBatisConfiguration { |
| | | // |
| | | // // 配置mapper的扫描,找到所有的mapper.xml映射文件 |
| | | //// @Value("${iailab.info.base-package}") |
| | | // @Value("${mybatis-plus.mapper-locations}") |
| | | // private String mapperLocations; |
| | | // |
| | | // @Bean |
| | | // @Primary |
| | | // public SqlSessionFactory sqlSessionFactory() throws Exception { |
| | | // SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); |
| | | // sqlSessionFactoryBean.setDataSource(DataSourceBuilder.create().build()); |
| | | // sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); |
| | | // sqlSessionFactoryBean.setConfiguration(buildConfiguration()); |
| | | // return sqlSessionFactoryBean.getObject(); |
| | | // } |
| | | // |
| | | // @Bean |
| | | // @Primary |
| | | // public SqlSessionTemplate sqlSessionTemplate() throws Exception { |
| | | // return new SqlSessionTemplate(sqlSessionFactory()); |
| | | // } |
| | | // |
| | | // @Bean |
| | | // @Primary |
| | | // public DataSourceTransactionManager transactionManager() { |
| | | // return new DataSourceTransactionManager(DataSourceBuilder.create().build()); |
| | | // } |
| | | // |
| | | // private org.apache.ibatis.session.Configuration buildConfiguration() { |
| | | // org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); |
| | | // configuration.getTypeHandlerRegistry().register(JSONObject.class, JdbcType.VARCHAR, MybatisHandler.class); |
| | | // configuration.getTypeHandlerRegistry().register(JSONArray.class, JdbcType.VARCHAR, MybatisHandler.class); |
| | | // return configuration; |
| | | // } |
| | | // |
| | | // @Bean |
| | | // public MybatisPlusInterceptor mybatisPlusInterceptor() { |
| | | // MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); |
| | | // // 数据权限 |
| | | // mybatisPlusInterceptor.addInnerInterceptor(new DataFilterInterceptor()); |
| | | // // 分页插件 |
| | | // mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); |
| | | // // 乐观锁 |
| | | // mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); |
| | | // // 防止全表更新与删除 |
| | | // mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); |
| | | // |
| | | // return mybatisPlusInterceptor; |
| | | // } |
| | | // |
| | | // |
| | | //} |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.dataobject; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.FieldFill; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableLogic; |
| | | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
| | | import com.fhs.core.trans.vo.TransPojo; |
| | | import lombok.Data; |
| | | import org.apache.ibatis.type.JdbcType; |
| | | |
| | | import java.io.Serializable; |
| | | import java.time.LocalDateTime; |
| | | |
| | | /** |
| | | * 基础实体对象 |
| | | * |
| | | * 为什么实现 {@link TransPojo} 接口? |
| | | * 因为使用 Easy-Trans TransType.SIMPLE 模式,集成 MyBatis Plus 查询 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Data |
| | | @JsonIgnoreProperties(value = "transMap") // 由于 Easy-Trans 会添加 transMap 属性,避免 Jackson 在 Spring Cache 反序列化报错 |
| | | public abstract class BaseDO implements Serializable, TransPojo { |
| | | |
| | | /** |
| | | * 创建时间 |
| | | */ |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private LocalDateTime createTime; |
| | | /** |
| | | * 最后更新时间 |
| | | */ |
| | | @TableField(fill = FieldFill.INSERT_UPDATE) |
| | | private LocalDateTime updateTime; |
| | | /** |
| | | * 创建者,目前使用 SysUser 的 id 编号 |
| | | * |
| | | * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 |
| | | */ |
| | | @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR) |
| | | private String creator; |
| | | /** |
| | | * 更新者,目前使用 SysUser 的 id 编号 |
| | | * |
| | | * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 |
| | | */ |
| | | @TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR) |
| | | private String updater; |
| | | /** |
| | | * 是否删除 |
| | | */ |
| | | @TableLogic |
| | | private Boolean deleted; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.enums; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.baomidou.mybatisplus.annotation.DbType; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Getter; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.Map; |
| | | import java.util.Optional; |
| | | import java.util.function.Function; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * 针对 MyBatis Plus 的 {@link DbType} 增强,补充更多信息 |
| | | */ |
| | | @Getter |
| | | @AllArgsConstructor |
| | | public enum DbTypeEnum { |
| | | |
| | | /** |
| | | * MySQL |
| | | */ |
| | | MY_SQL( DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"), |
| | | |
| | | /** |
| | | * Oracle |
| | | */ |
| | | ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"), |
| | | |
| | | /** |
| | | * PostgreSQL |
| | | * |
| | | * 华为 openGauss 使用 ProductName 与 PostgreSQL 相同 |
| | | */ |
| | | POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"), |
| | | |
| | | /** |
| | | * SQL Server |
| | | */ |
| | | SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"), |
| | | |
| | | /** |
| | | * 达梦 |
| | | */ |
| | | DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"), |
| | | |
| | | /** |
| | | * 人大金仓 |
| | | */ |
| | | KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"), |
| | | ; |
| | | |
| | | public static final Map<String, DbTypeEnum> MAP_BY_NAME = Arrays.stream(values()) |
| | | .collect(Collectors.toMap(DbTypeEnum::getProductName, Function.identity())); |
| | | |
| | | public static final Map<DbType, DbTypeEnum> MAP_BY_MP = Arrays.stream(values()) |
| | | .collect(Collectors.toMap(DbTypeEnum::getMpDbType, Function.identity())); |
| | | |
| | | /** |
| | | * MyBatis Plus 类型 |
| | | */ |
| | | private final DbType mpDbType; |
| | | /** |
| | | * 数据库产品名 |
| | | */ |
| | | private final String productName; |
| | | /** |
| | | * SQL FIND_IN_SET 模板 |
| | | */ |
| | | private final String findInSetTemplate; |
| | | |
| | | public static DbType find(String databaseProductName) { |
| | | if (StrUtil.isBlank(databaseProductName)) { |
| | | return null; |
| | | } |
| | | return MAP_BY_NAME.get(databaseProductName).getMpDbType(); |
| | | } |
| | | |
| | | public static String getFindInSetTemplate(DbType dbType) { |
| | | return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate()) |
| | | .orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported")); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.enums; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.DbType; |
| | | |
| | | /** |
| | | * SQL相关常量类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class SqlConstants { |
| | | |
| | | /** |
| | | * 数据库的类型 |
| | | */ |
| | | // TODO 此处使用nacos配置文件的话,先初始化nacos配置才初始化dbType,导致读不到值。暂时先固定Mysql |
| | | public static DbType DB_TYPE = DbType.MYSQL; |
| | | |
| | | public static void init(DbType dbType) { |
| | | DB_TYPE = dbType; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.handler; |
| | | |
| | | import com.iailab.framework.mybatis.core.dataobject.BaseDO; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; |
| | | import org.apache.ibatis.reflection.MetaObject; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.Objects; |
| | | |
| | | /** |
| | | * 通用参数填充实现类 |
| | | * |
| | | * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值 |
| | | * |
| | | * @author hexiaowu |
| | | */ |
| | | public class DefaultDBFieldHandler implements MetaObjectHandler { |
| | | |
| | | @Override |
| | | public void insertFill(MetaObject metaObject) { |
| | | if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) { |
| | | BaseDO baseDO = (BaseDO) metaObject.getOriginalObject(); |
| | | |
| | | LocalDateTime current = LocalDateTime.now(); |
| | | // 创建时间为空,则以当前时间为插入时间 |
| | | if (Objects.isNull(baseDO.getCreateTime())) { |
| | | baseDO.setCreateTime(current); |
| | | } |
| | | // 更新时间为空,则以当前时间为更新时间 |
| | | if (Objects.isNull(baseDO.getUpdateTime())) { |
| | | baseDO.setUpdateTime(current); |
| | | } |
| | | |
| | | Long userId = WebFrameworkUtils.getLoginUserId(); |
| | | // 当前登录用户不为空,创建人为空,则当前登录用户为创建人 |
| | | if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) { |
| | | baseDO.setCreator(userId.toString()); |
| | | } |
| | | // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 |
| | | if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) { |
| | | baseDO.setUpdater(userId.toString()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void updateFill(MetaObject metaObject) { |
| | | // 更新时间为空,则以当前时间为更新时间 |
| | | Object modifyTime = getFieldValByName("updateTime", metaObject); |
| | | if (Objects.isNull(modifyTime)) { |
| | | setFieldValByName("updateTime", LocalDateTime.now(), metaObject); |
| | | } |
| | | |
| | | // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 |
| | | Object modifier = getFieldValByName("updater", metaObject); |
| | | Long userId = WebFrameworkUtils.getLoginUserId(); |
| | | if (Objects.nonNull(userId) && Objects.isNull(modifier)) { |
| | | setFieldValByName("updater", userId.toString(), metaObject); |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.handler; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import org.apache.ibatis.type.BaseTypeHandler; |
| | | import org.apache.ibatis.type.JdbcType; |
| | | |
| | | import java.sql.CallableStatement; |
| | | import java.sql.PreparedStatement; |
| | | import java.sql.ResultSet; |
| | | import java.sql.SQLException; |
| | | |
| | | public class MybatisHandler extends BaseTypeHandler<Object> { |
| | | |
| | | @Override |
| | | public void setNonNullParameter(PreparedStatement preparedStatement, int i, Object o, JdbcType jdbcType) throws SQLException { |
| | | preparedStatement.setString(i, JSON.toJSONString(o)); |
| | | } |
| | | |
| | | @Override |
| | | public Object getNullableResult(ResultSet resultSet, String s) throws SQLException { |
| | | String sqlJson = resultSet.getString(s); |
| | | if (null != sqlJson) { |
| | | return JSON.parse(sqlJson); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Object getNullableResult(ResultSet resultSet, int i) throws SQLException { |
| | | String sqlJson = resultSet.getString(i); |
| | | if (null != sqlJson) { |
| | | return JSON.parse(sqlJson); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Object getNullableResult(CallableStatement callableStatement, int i) throws SQLException { |
| | | String sqlJson = callableStatement.getString(i); |
| | | if (null != sqlJson) { |
| | | return JSON.parse(sqlJson); |
| | | } |
| | | return null; |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.mapper; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.iailab.framework.common.pojo.PageParam; |
| | | import com.iailab.framework.common.pojo.PageResult; |
| | | import com.iailab.framework.common.pojo.SortablePageParam; |
| | | import com.iailab.framework.common.pojo.SortingField; |
| | | import com.iailab.framework.mybatis.core.enums.SqlConstants; |
| | | import com.iailab.framework.mybatis.core.util.MyBatisUtils; |
| | | import com.baomidou.mybatisplus.annotation.DbType; |
| | | import com.baomidou.mybatisplus.core.conditions.Wrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.core.toolkit.support.SFunction; |
| | | import com.baomidou.mybatisplus.extension.toolkit.Db; |
| | | import com.github.yulichang.base.MPJBaseMapper; |
| | | import com.github.yulichang.interfaces.MPJBaseJoin; |
| | | import com.github.yulichang.wrapper.MPJLambdaWrapper; |
| | | import org.apache.ibatis.annotations.Param; |
| | | |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.Objects; |
| | | |
| | | /** |
| | | * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 |
| | | * |
| | | * 1. {@link BaseMapper} 为 MyBatis Plus 的基础接口,提供基础的 CRUD 能力 |
| | | * 2. {@link MPJBaseMapper} 为 MyBatis Plus Join 的基础接口,提供连表 Join 能力 |
| | | */ |
| | | public interface BaseMapperX<T> extends MPJBaseMapper<T> { |
| | | |
| | | /** |
| | | * 获取分页对象 |
| | | * @param params 分页查询参数 |
| | | */ |
| | | default IPage<T> getPage(PageParam params) { |
| | | //分页参数 |
| | | long curPage = 1; |
| | | long limit = 10; |
| | | |
| | | if(params.getPageNo() != null){ |
| | | curPage = params.getPageNo(); |
| | | } |
| | | if(params.getPageSize() != null){ |
| | | limit = params.getPageSize(); |
| | | } |
| | | |
| | | //分页对象 |
| | | return new Page<>(curPage, limit); |
| | | } |
| | | |
| | | default PageResult<T> selectPage(SortablePageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) { |
| | | return selectPage(pageParam, pageParam.getSortingFields(), queryWrapper); |
| | | } |
| | | |
| | | default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) { |
| | | return selectPage(pageParam, null, queryWrapper); |
| | | } |
| | | |
| | | default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) { |
| | | // 特殊:不分页,直接查询全部 |
| | | if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { |
| | | List<T> list = selectList(queryWrapper); |
| | | return new PageResult<>(list, (long) list.size()); |
| | | } |
| | | |
| | | // MyBatis Plus 查询 |
| | | IPage<T> mpPage = MyBatisUtils.buildPage(pageParam, sortingFields); |
| | | selectPage(mpPage, queryWrapper); |
| | | // 转换返回 |
| | | return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); |
| | | } |
| | | |
| | | default <D> PageResult<D> selectJoinPage(PageParam pageParam, Class<D> clazz, MPJLambdaWrapper<T> lambdaWrapper) { |
| | | // 特殊:不分页,直接查询全部 |
| | | if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { |
| | | List<D> list = selectJoinList(clazz, lambdaWrapper); |
| | | return new PageResult<>(list, (long) list.size()); |
| | | } |
| | | |
| | | // MyBatis Plus Join 查询 |
| | | IPage<D> mpPage = MyBatisUtils.buildPage(pageParam); |
| | | mpPage = selectJoinPage(mpPage, clazz, lambdaWrapper); |
| | | // 转换返回 |
| | | return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); |
| | | } |
| | | |
| | | default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) { |
| | | IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam); |
| | | selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper); |
| | | // 转换返回 |
| | | return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); |
| | | } |
| | | |
| | | default T selectOne(String field, Object value) { |
| | | return selectOne(new QueryWrapper<T>().eq(field, value)); |
| | | } |
| | | |
| | | default T selectOne(SFunction<T, ?> field, Object value) { |
| | | return selectOne(new LambdaQueryWrapper<T>().eq(field, value)); |
| | | } |
| | | |
| | | default T selectOne(String field1, Object value1, String field2, Object value2) { |
| | | return selectOne(new QueryWrapper<T>().eq(field1, value1).eq(field2, value2)); |
| | | } |
| | | |
| | | default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) { |
| | | return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)); |
| | | } |
| | | |
| | | default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2, |
| | | SFunction<T, ?> field3, Object value3) { |
| | | return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2) |
| | | .eq(field3, value3)); |
| | | } |
| | | |
| | | default Long selectCount() { |
| | | return selectCount(new QueryWrapper<>()); |
| | | } |
| | | |
| | | default Long selectCount(String field, Object value) { |
| | | return selectCount(new QueryWrapper<T>().eq(field, value)); |
| | | } |
| | | |
| | | default Long selectCount(SFunction<T, ?> field, Object value) { |
| | | return selectCount(new LambdaQueryWrapper<T>().eq(field, value)); |
| | | } |
| | | |
| | | default List<T> selectList() { |
| | | return selectList(new QueryWrapper<>()); |
| | | } |
| | | |
| | | default List<T> selectList(String field, Object value) { |
| | | return selectList(new QueryWrapper<T>().eq(field, value)); |
| | | } |
| | | |
| | | default List<T> selectList(SFunction<T, ?> field, Object value) { |
| | | return selectList(new LambdaQueryWrapper<T>().eq(field, value)); |
| | | } |
| | | |
| | | default List<T> selectList(String field, Collection<?> values) { |
| | | if (CollUtil.isEmpty(values)) { |
| | | return CollUtil.newArrayList(); |
| | | } |
| | | return selectList(new QueryWrapper<T>().in(field, values)); |
| | | } |
| | | |
| | | default List<T> selectList(SFunction<T, ?> field, Collection<?> values) { |
| | | if (CollUtil.isEmpty(values)) { |
| | | return CollUtil.newArrayList(); |
| | | } |
| | | return selectList(new LambdaQueryWrapper<T>().in(field, values)); |
| | | } |
| | | |
| | | @Deprecated |
| | | default List<T> selectList(SFunction<T, ?> leField, SFunction<T, ?> geField, Object value) { |
| | | return selectList(new LambdaQueryWrapper<T>().le(leField, value).ge(geField, value)); |
| | | } |
| | | |
| | | default List<T> selectList(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) { |
| | | return selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)); |
| | | } |
| | | |
| | | /** |
| | | * 批量插入,适合大量数据插入 |
| | | * |
| | | * @param entities 实体们 |
| | | */ |
| | | default Boolean insertBatch(Collection<T> entities) { |
| | | // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理 |
| | | if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) { |
| | | entities.forEach(this::insert); |
| | | return CollUtil.isNotEmpty(entities); |
| | | } |
| | | return Db.saveBatch(entities); |
| | | } |
| | | |
| | | /** |
| | | * 批量插入,适合大量数据插入 |
| | | * |
| | | * @param entities 实体们 |
| | | * @param size 插入数量 Db.saveBatch 默认为 1000 |
| | | */ |
| | | default Boolean insertBatch(Collection<T> entities, int size) { |
| | | // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理 |
| | | if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) { |
| | | entities.forEach(this::insert); |
| | | return CollUtil.isNotEmpty(entities); |
| | | } |
| | | return Db.saveBatch(entities, size); |
| | | } |
| | | |
| | | default int updateBatch(T update) { |
| | | return update(update, new QueryWrapper<>()); |
| | | } |
| | | |
| | | default Boolean updateBatch(Collection<T> entities) { |
| | | return Db.updateBatchById(entities); |
| | | } |
| | | |
| | | default Boolean updateBatch(Collection<T> entities, int size) { |
| | | return Db.updateBatchById(entities, size); |
| | | } |
| | | |
| | | default boolean insertOrUpdate(T entity) { |
| | | return Db.saveOrUpdate(entity); |
| | | } |
| | | |
| | | default Boolean insertOrUpdateBatch(Collection<T> collection) { |
| | | return Db.saveOrUpdateBatch(collection); |
| | | } |
| | | |
| | | default int delete(String field, String value) { |
| | | return delete(new QueryWrapper<T>().eq(field, value)); |
| | | } |
| | | |
| | | default int delete(SFunction<T, ?> field, Object value) { |
| | | return delete(new LambdaQueryWrapper<T>().eq(field, value)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.query; |
| | | |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import com.iailab.framework.common.util.collection.ArrayUtils; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.toolkit.support.SFunction; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.util.Collection; |
| | | |
| | | /** |
| | | * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: |
| | | * <p> |
| | | * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 |
| | | * |
| | | * @param <T> 数据类型 |
| | | */ |
| | | public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> { |
| | | |
| | | public LambdaQueryWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) { |
| | | if (StringUtils.hasText(val)) { |
| | | return (LambdaQueryWrapperX<T>) super.like(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) { |
| | | if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { |
| | | return (LambdaQueryWrapperX<T>) super.in(column, values); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) { |
| | | if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { |
| | | return (LambdaQueryWrapperX<T>) super.in(column, values); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public LambdaQueryWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (ObjectUtil.isNotEmpty(val)) { |
| | | return (LambdaQueryWrapperX<T>) super.eq(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public LambdaQueryWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (ObjectUtil.isNotEmpty(val)) { |
| | | return (LambdaQueryWrapperX<T>) super.ne(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public LambdaQueryWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (val != null) { |
| | | return (LambdaQueryWrapperX<T>) super.gt(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public LambdaQueryWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (val != null) { |
| | | return (LambdaQueryWrapperX<T>) super.ge(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public LambdaQueryWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (val != null) { |
| | | return (LambdaQueryWrapperX<T>) super.lt(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public LambdaQueryWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (val != null) { |
| | | return (LambdaQueryWrapperX<T>) super.le(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) { |
| | | if (val1 != null && val2 != null) { |
| | | return (LambdaQueryWrapperX<T>) super.between(column, val1, val2); |
| | | } |
| | | if (val1 != null) { |
| | | return (LambdaQueryWrapperX<T>) ge(column, val1); |
| | | } |
| | | if (val2 != null) { |
| | | return (LambdaQueryWrapperX<T>) le(column, val2); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) { |
| | | Object val1 = ArrayUtils.get(values, 0); |
| | | Object val2 = ArrayUtils.get(values, 1); |
| | | return betweenIfPresent(column, val1, val2); |
| | | } |
| | | |
| | | // ========== 重写父类方法,方便链式调用 ========== |
| | | |
| | | @Override |
| | | public LambdaQueryWrapperX<T> eq(boolean condition, SFunction<T, ?> column, Object val) { |
| | | super.eq(condition, column, val); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public LambdaQueryWrapperX<T> eq(SFunction<T, ?> column, Object val) { |
| | | super.eq(column, val); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public LambdaQueryWrapperX<T> orderByDesc(SFunction<T, ?> column) { |
| | | super.orderByDesc(true, column); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public LambdaQueryWrapperX<T> last(String lastSql) { |
| | | super.last(lastSql); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public LambdaQueryWrapperX<T> in(SFunction<T, ?> column, Collection<?> coll) { |
| | | super.in(column, coll); |
| | | return this; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.query; |
| | | |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import com.iailab.framework.common.util.collection.ArrayUtils; |
| | | import com.baomidou.mybatisplus.core.toolkit.support.SFunction; |
| | | import com.github.yulichang.toolkit.MPJWrappers; |
| | | import com.github.yulichang.wrapper.MPJLambdaWrapper; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.util.Collection; |
| | | import java.util.function.Consumer; |
| | | |
| | | /** |
| | | * 拓展 MyBatis Plus Join QueryWrapper 类,主要增加如下功能: |
| | | * <p> |
| | | * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 |
| | | * |
| | | * @param <T> 数据类型 |
| | | */ |
| | | public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> { |
| | | |
| | | public MPJLambdaWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) { |
| | | MPJWrappers.lambdaJoin().like(column, val); |
| | | if (StringUtils.hasText(val)) { |
| | | return (MPJLambdaWrapperX<T>) super.like(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) { |
| | | if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { |
| | | return (MPJLambdaWrapperX<T>) super.in(column, values); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) { |
| | | if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { |
| | | return (MPJLambdaWrapperX<T>) super.in(column, values); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public MPJLambdaWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (ObjectUtil.isNotEmpty(val)) { |
| | | return (MPJLambdaWrapperX<T>) super.eq(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public MPJLambdaWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (ObjectUtil.isNotEmpty(val)) { |
| | | return (MPJLambdaWrapperX<T>) super.ne(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public MPJLambdaWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (val != null) { |
| | | return (MPJLambdaWrapperX<T>) super.gt(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public MPJLambdaWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (val != null) { |
| | | return (MPJLambdaWrapperX<T>) super.ge(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public MPJLambdaWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (val != null) { |
| | | return (MPJLambdaWrapperX<T>) super.lt(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public MPJLambdaWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) { |
| | | if (val != null) { |
| | | return (MPJLambdaWrapperX<T>) super.le(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) { |
| | | if (val1 != null && val2 != null) { |
| | | return (MPJLambdaWrapperX<T>) super.between(column, val1, val2); |
| | | } |
| | | if (val1 != null) { |
| | | return (MPJLambdaWrapperX<T>) ge(column, val1); |
| | | } |
| | | if (val2 != null) { |
| | | return (MPJLambdaWrapperX<T>) le(column, val2); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) { |
| | | Object val1 = ArrayUtils.get(values, 0); |
| | | Object val2 = ArrayUtils.get(values, 1); |
| | | return betweenIfPresent(column, val1, val2); |
| | | } |
| | | |
| | | // ========== 重写父类方法,方便链式调用 ========== |
| | | |
| | | @Override |
| | | public <X> MPJLambdaWrapperX<T> eq(boolean condition, SFunction<X, ?> column, Object val) { |
| | | super.eq(condition, column, val); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <X> MPJLambdaWrapperX<T> eq(SFunction<X, ?> column, Object val) { |
| | | super.eq(column, val); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <X> MPJLambdaWrapperX<T> orderByDesc(SFunction<X, ?> column) { |
| | | //noinspection unchecked |
| | | super.orderByDesc(true, column); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public MPJLambdaWrapperX<T> last(String lastSql) { |
| | | super.last(lastSql); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <X> MPJLambdaWrapperX<T> in(SFunction<X, ?> column, Collection<?> coll) { |
| | | super.in(column, coll); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public MPJLambdaWrapperX<T> selectAll(Class<?> clazz) { |
| | | super.selectAll(clazz); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public MPJLambdaWrapperX<T> selectAll(Class<?> clazz, String prefix) { |
| | | super.selectAll(clazz, prefix); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, String alias) { |
| | | super.selectAs(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <E> MPJLambdaWrapperX<T> selectAs(String column, SFunction<E, ?> alias) { |
| | | super.selectAs(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, SFunction<X, ?> alias) { |
| | | super.selectAs(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <E, X> MPJLambdaWrapperX<T> selectAs(String index, SFunction<E, ?> column, SFunction<X, ?> alias) { |
| | | super.selectAs(index, column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <E> MPJLambdaWrapperX<T> selectAsClass(Class<E> source, Class<?> tag) { |
| | | super.selectAsClass(source, tag); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) { |
| | | super.selectSub(clazz, consumer, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, String st, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) { |
| | | super.selectSub(clazz, st, consumer, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column) { |
| | | super.selectCount(column); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public MPJLambdaWrapperX<T> selectCount(Object column, String alias) { |
| | | super.selectCount(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <X> MPJLambdaWrapperX<T> selectCount(Object column, SFunction<X, ?> alias) { |
| | | super.selectCount(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, String alias) { |
| | | super.selectCount(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, SFunction<X, ?> alias) { |
| | | super.selectCount(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column) { |
| | | super.selectSum(column); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, String alias) { |
| | | super.selectSum(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, SFunction<X, ?> alias) { |
| | | super.selectSum(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column) { |
| | | super.selectMax(column); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, String alias) { |
| | | super.selectMax(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, SFunction<X, ?> alias) { |
| | | super.selectMax(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column) { |
| | | super.selectMin(column); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, String alias) { |
| | | super.selectMin(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, SFunction<X, ?> alias) { |
| | | super.selectMin(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column) { |
| | | super.selectAvg(column); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, String alias) { |
| | | super.selectAvg(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, SFunction<X, ?> alias) { |
| | | super.selectAvg(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column) { |
| | | super.selectLen(column); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, String alias) { |
| | | super.selectLen(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, SFunction<X, ?> alias) { |
| | | super.selectLen(column, alias); |
| | | return this; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.query; |
| | | |
| | | import cn.hutool.core.lang.Assert; |
| | | import com.iailab.framework.mybatis.core.enums.SqlConstants; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; |
| | | import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.util.Collection; |
| | | |
| | | /** |
| | | * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: |
| | | * |
| | | * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 |
| | | * |
| | | * @param <T> 数据类型 |
| | | */ |
| | | public class QueryWrapperX<T> extends QueryWrapper<T> { |
| | | |
| | | public QueryWrapperX<T> likeIfPresent(String column, String val) { |
| | | if (StringUtils.hasText(val)) { |
| | | return (QueryWrapperX<T>) super.like(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public QueryWrapperX<T> inIfPresent(String column, Collection<?> values) { |
| | | if (!CollectionUtils.isEmpty(values)) { |
| | | return (QueryWrapperX<T>) super.in(column, values); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public QueryWrapperX<T> inIfPresent(String column, Object... values) { |
| | | if (!ArrayUtils.isEmpty(values)) { |
| | | return (QueryWrapperX<T>) super.in(column, values); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public QueryWrapperX<T> eqIfPresent(String column, Object val) { |
| | | if (val != null) { |
| | | return (QueryWrapperX<T>) super.eq(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public QueryWrapperX<T> neIfPresent(String column, Object val) { |
| | | if (val != null) { |
| | | return (QueryWrapperX<T>) super.ne(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public QueryWrapperX<T> gtIfPresent(String column, Object val) { |
| | | if (val != null) { |
| | | return (QueryWrapperX<T>) super.gt(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public QueryWrapperX<T> geIfPresent(String column, Object val) { |
| | | if (val != null) { |
| | | return (QueryWrapperX<T>) super.ge(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public QueryWrapperX<T> ltIfPresent(String column, Object val) { |
| | | if (val != null) { |
| | | return (QueryWrapperX<T>) super.lt(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public QueryWrapperX<T> leIfPresent(String column, Object val) { |
| | | if (val != null) { |
| | | return (QueryWrapperX<T>) super.le(column, val); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public QueryWrapperX<T> betweenIfPresent(String column, Object val1, Object val2) { |
| | | if (val1 != null && val2 != null) { |
| | | return (QueryWrapperX<T>) super.between(column, val1, val2); |
| | | } |
| | | if (val1 != null) { |
| | | return (QueryWrapperX<T>) ge(column, val1); |
| | | } |
| | | if (val2 != null) { |
| | | return (QueryWrapperX<T>) le(column, val2); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) { |
| | | if (values!= null && values.length != 0 && values[0] != null && values[1] != null) { |
| | | return (QueryWrapperX<T>) super.between(column, values[0], values[1]); |
| | | } |
| | | if (values!= null && values.length != 0 && values[0] != null) { |
| | | return (QueryWrapperX<T>) ge(column, values[0]); |
| | | } |
| | | if (values!= null && values.length != 0 && values[1] != null) { |
| | | return (QueryWrapperX<T>) le(column, values[1]); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | // ========== 重写父类方法,方便链式调用 ========== |
| | | |
| | | @Override |
| | | public QueryWrapperX<T> eq(boolean condition, String column, Object val) { |
| | | super.eq(condition, column, val); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public QueryWrapperX<T> eq(String column, Object val) { |
| | | super.eq(column, val); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public QueryWrapperX<T> orderByDesc(String column) { |
| | | super.orderByDesc(true, column); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public QueryWrapperX<T> last(String lastSql) { |
| | | super.last(lastSql); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public QueryWrapperX<T> in(String column, Collection<?> coll) { |
| | | super.in(column, coll); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 设置只返回最后一条 |
| | | * |
| | | * TODO iailab:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同 |
| | | * |
| | | * @return this |
| | | */ |
| | | public QueryWrapperX<T> limitN(int n) { |
| | | Assert.notNull(SqlConstants.DB_TYPE, "获取不到数据库的类型"); |
| | | switch (SqlConstants.DB_TYPE) { |
| | | case ORACLE: |
| | | case ORACLE_12C: |
| | | super.le("ROWNUM", n); |
| | | break; |
| | | case SQL_SERVER: |
| | | case SQL_SERVER2005: |
| | | super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段 |
| | | break; |
| | | default: |
| | | super.last("LIMIT " + n); |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.type; |
| | | |
| | | import cn.hutool.core.lang.Assert; |
| | | import cn.hutool.crypto.SecureUtil; |
| | | import cn.hutool.crypto.symmetric.AES; |
| | | import cn.hutool.extra.spring.SpringUtil; |
| | | import org.apache.ibatis.type.BaseTypeHandler; |
| | | import org.apache.ibatis.type.JdbcType; |
| | | |
| | | import java.sql.CallableStatement; |
| | | import java.sql.PreparedStatement; |
| | | import java.sql.ResultSet; |
| | | import java.sql.SQLException; |
| | | |
| | | /** |
| | | * 字段字段的 TypeHandler 实现类,基于 {@link cn.hutool.crypto.symmetric.AES} 实现 |
| | | * 可通过 jasypt.encryptor.password 配置项,设置密钥 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class EncryptTypeHandler extends BaseTypeHandler<String> { |
| | | |
| | | private static final String ENCRYPTOR_PROPERTY_NAME = "mybatis-plus.encryptor.password"; |
| | | |
| | | private static AES aes; |
| | | |
| | | @Override |
| | | public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { |
| | | ps.setString(i, encrypt(parameter)); |
| | | } |
| | | |
| | | @Override |
| | | public String getNullableResult(ResultSet rs, String columnName) throws SQLException { |
| | | String value = rs.getString(columnName); |
| | | return decrypt(value); |
| | | } |
| | | |
| | | @Override |
| | | public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { |
| | | String value = rs.getString(columnIndex); |
| | | return decrypt(value); |
| | | } |
| | | |
| | | @Override |
| | | public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { |
| | | String value = cs.getString(columnIndex); |
| | | return decrypt(value); |
| | | } |
| | | |
| | | private static String decrypt(String value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | return getEncryptor().decryptStr(value); |
| | | } |
| | | |
| | | public static String encrypt(String rawValue) { |
| | | if (rawValue == null) { |
| | | return null; |
| | | } |
| | | return getEncryptor().encryptBase64(rawValue); |
| | | } |
| | | |
| | | private static AES getEncryptor() { |
| | | if (aes != null) { |
| | | return aes; |
| | | } |
| | | // 构建 AES |
| | | String password = SpringUtil.getProperty(ENCRYPTOR_PROPERTY_NAME); |
| | | Assert.notEmpty(password, "配置项({}) 不能为空", ENCRYPTOR_PROPERTY_NAME); |
| | | aes = SecureUtil.aes(password.getBytes()); |
| | | return aes; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.type; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.iailab.framework.common.util.string.StrUtils; |
| | | import org.apache.ibatis.type.JdbcType; |
| | | import org.apache.ibatis.type.MappedJdbcTypes; |
| | | import org.apache.ibatis.type.MappedTypes; |
| | | import org.apache.ibatis.type.TypeHandler; |
| | | |
| | | import java.sql.CallableStatement; |
| | | import java.sql.PreparedStatement; |
| | | import java.sql.ResultSet; |
| | | import java.sql.SQLException; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * List<Integer> 的类型转换器实现类,对应数据库的 varchar 类型 |
| | | * |
| | | * @author jason |
| | | */ |
| | | @MappedJdbcTypes(JdbcType.VARCHAR) |
| | | @MappedTypes(List.class) |
| | | public class IntegerListTypeHandler implements TypeHandler<List<Integer>> { |
| | | |
| | | private static final String COMMA = ","; |
| | | |
| | | @Override |
| | | public void setParameter(PreparedStatement ps, int i, List<Integer> strings, JdbcType jdbcType) throws SQLException { |
| | | ps.setString(i, CollUtil.join(strings, COMMA)); |
| | | } |
| | | |
| | | @Override |
| | | public List<Integer> getResult(ResultSet rs, String columnName) throws SQLException { |
| | | String value = rs.getString(columnName); |
| | | return getResult(value); |
| | | } |
| | | |
| | | @Override |
| | | public List<Integer> getResult(ResultSet rs, int columnIndex) throws SQLException { |
| | | String value = rs.getString(columnIndex); |
| | | return getResult(value); |
| | | } |
| | | |
| | | @Override |
| | | public List<Integer> getResult(CallableStatement cs, int columnIndex) throws SQLException { |
| | | String value = cs.getString(columnIndex); |
| | | return getResult(value); |
| | | } |
| | | |
| | | private List<Integer> getResult(String value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | return StrUtils.splitToInteger(value, COMMA); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | //package com.iailab.framework.mybatis.core.type; |
| | | // |
| | | //import com.iailab.framework.common.util.json.JsonUtils; |
| | | //import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; |
| | | //import com.fasterxml.jackson.core.type.TypeReference; |
| | | // |
| | | //import java.util.Set; |
| | | // |
| | | ///** |
| | | // * 参考 {@link com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} 实现 |
| | | // * 在我们将字符串反序列化为 Set 并且泛型为 Long 时,如果每个元素的数值太小,会被处理成 Integer 类型,导致可能存在隐性的 BUG。 |
| | | // * |
| | | // * 例如说哦,SysUserDO 的 postIds 属性 |
| | | // * |
| | | // * @author iailab |
| | | // */ |
| | | //public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler<Object> { |
| | | // |
| | | // private static final TypeReference<Set<Long>> TYPE_REFERENCE = new TypeReference<Set<Long>>(){}; |
| | | // |
| | | // @Override |
| | | // protected Object parse(String json) { |
| | | // return JsonUtils.parseObject(json, TYPE_REFERENCE); |
| | | // } |
| | | // |
| | | // @Override |
| | | // protected String toJson(Object obj) { |
| | | // return JsonUtils.toJsonString(obj); |
| | | // } |
| | | // |
| | | //} |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.type; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.iailab.framework.common.util.string.StrUtils; |
| | | import org.apache.ibatis.type.JdbcType; |
| | | import org.apache.ibatis.type.MappedJdbcTypes; |
| | | import org.apache.ibatis.type.MappedTypes; |
| | | import org.apache.ibatis.type.TypeHandler; |
| | | |
| | | import java.sql.CallableStatement; |
| | | import java.sql.PreparedStatement; |
| | | import java.sql.ResultSet; |
| | | import java.sql.SQLException; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * List<Long> 的类型转换器实现类,对应数据库的 varchar 类型 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @MappedJdbcTypes(JdbcType.VARCHAR) |
| | | @MappedTypes(List.class) |
| | | public class LongListTypeHandler implements TypeHandler<List<Long>> { |
| | | |
| | | private static final String COMMA = ","; |
| | | |
| | | @Override |
| | | public void setParameter(PreparedStatement ps, int i, List<Long> strings, JdbcType jdbcType) throws SQLException { |
| | | // 设置占位符 |
| | | ps.setString(i, CollUtil.join(strings, COMMA)); |
| | | } |
| | | |
| | | @Override |
| | | public List<Long> getResult(ResultSet rs, String columnName) throws SQLException { |
| | | String value = rs.getString(columnName); |
| | | return getResult(value); |
| | | } |
| | | |
| | | @Override |
| | | public List<Long> getResult(ResultSet rs, int columnIndex) throws SQLException { |
| | | String value = rs.getString(columnIndex); |
| | | return getResult(value); |
| | | } |
| | | |
| | | @Override |
| | | public List<Long> getResult(CallableStatement cs, int columnIndex) throws SQLException { |
| | | String value = cs.getString(columnIndex); |
| | | return getResult(value); |
| | | } |
| | | |
| | | private List<Long> getResult(String value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | return StrUtils.splitToLong(value, COMMA); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.type; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import org.apache.ibatis.type.JdbcType; |
| | | import org.apache.ibatis.type.MappedJdbcTypes; |
| | | import org.apache.ibatis.type.MappedTypes; |
| | | import org.apache.ibatis.type.TypeHandler; |
| | | |
| | | import java.sql.CallableStatement; |
| | | import java.sql.PreparedStatement; |
| | | import java.sql.ResultSet; |
| | | import java.sql.SQLException; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * List<String> 的类型转换器实现类,对应数据库的 varchar 类型 |
| | | * |
| | | * @author 永不言败 |
| | | * @since 2022 3/23 12:50:15 |
| | | */ |
| | | @MappedJdbcTypes(JdbcType.VARCHAR) |
| | | @MappedTypes(List.class) |
| | | public class StringListTypeHandler implements TypeHandler<List<String>> { |
| | | |
| | | private static final String COMMA = ","; |
| | | |
| | | @Override |
| | | public void setParameter(PreparedStatement ps, int i, List<String> strings, JdbcType jdbcType) throws SQLException { |
| | | // 设置占位符 |
| | | ps.setString(i, CollUtil.join(strings, COMMA)); |
| | | } |
| | | |
| | | @Override |
| | | public List<String> getResult(ResultSet rs, String columnName) throws SQLException { |
| | | String value = rs.getString(columnName); |
| | | return getResult(value); |
| | | } |
| | | |
| | | @Override |
| | | public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException { |
| | | String value = rs.getString(columnIndex); |
| | | return getResult(value); |
| | | } |
| | | |
| | | @Override |
| | | public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException { |
| | | String value = cs.getString(columnIndex); |
| | | return getResult(value); |
| | | } |
| | | |
| | | private List<String> getResult(String value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | return StrUtil.splitTrim(value, COMMA); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.util; |
| | | |
| | | import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; |
| | | import com.baomidou.mybatisplus.annotation.DbType; |
| | | import com.iailab.framework.common.util.spring.SpringUtils; |
| | | import com.iailab.framework.mybatis.core.enums.DbTypeEnum; |
| | | |
| | | import javax.sql.DataSource; |
| | | import java.sql.Connection; |
| | | import java.sql.DriverManager; |
| | | import java.sql.SQLException; |
| | | |
| | | /** |
| | | * JDBC 工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class JdbcUtils { |
| | | |
| | | /** |
| | | * 判断连接是否正确 |
| | | * |
| | | * @param url 数据源连接 |
| | | * @param username 账号 |
| | | * @param password 密码 |
| | | * @return 是否正确 |
| | | */ |
| | | public static boolean isConnectionOK(String url, String username, String password) { |
| | | try (Connection ignored = DriverManager.getConnection(url, username, password)) { |
| | | return true; |
| | | } catch (Exception ex) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获得 URL 对应的 DB 类型 |
| | | * |
| | | * @param url URL |
| | | * @return DB 类型 |
| | | */ |
| | | public static DbType getDbType(String url) { |
| | | return com.baomidou.mybatisplus.extension.toolkit.JdbcUtils.getDbType(url); |
| | | } |
| | | |
| | | /** |
| | | * 通过当前数据库连接获得对应的 DB 类型 |
| | | * |
| | | * @return DB 类型 |
| | | */ |
| | | public static DbType getDbType() { |
| | | DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class); |
| | | DataSource dataSource = dynamicRoutingDataSource.determineDataSource(); |
| | | try (Connection conn = dataSource.getConnection()) { |
| | | return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName()); |
| | | } catch (SQLException e) { |
| | | throw new IllegalArgumentException(e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.mybatis.core.util; |
| | | |
| | | import cn.hutool.core.collection.CollectionUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.baomidou.mybatisplus.annotation.DbType; |
| | | import com.baomidou.mybatisplus.core.metadata.OrderItem; |
| | | import com.baomidou.mybatisplus.core.toolkit.StringPool; |
| | | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
| | | import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.iailab.framework.common.pojo.PageParam; |
| | | import com.iailab.framework.common.pojo.SortingField; |
| | | import com.iailab.framework.mybatis.core.enums.DbTypeEnum; |
| | | import net.sf.jsqlparser.expression.Alias; |
| | | import net.sf.jsqlparser.schema.Column; |
| | | import net.sf.jsqlparser.schema.Table; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * MyBatis 工具类 |
| | | */ |
| | | public class MyBatisUtils { |
| | | |
| | | private static final String MYSQL_ESCAPE_CHARACTER = "`"; |
| | | |
| | | public static <T> Page<T> buildPage(PageParam pageParam) { |
| | | return buildPage(pageParam, null); |
| | | } |
| | | |
| | | public static <T> Page<T> buildPage(PageParam pageParam, Collection<SortingField> sortingFields) { |
| | | // 页码 + 数量 |
| | | Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize()); |
| | | // 排序字段 |
| | | if (!CollectionUtil.isEmpty(sortingFields)) { |
| | | page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? |
| | | OrderItem.asc(sortingField.getField()) : OrderItem.desc(sortingField.getField())) |
| | | .collect(Collectors.toList())); |
| | | } |
| | | return page; |
| | | } |
| | | |
| | | /** |
| | | * 将拦截器添加到链中 |
| | | * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 |
| | | * |
| | | * @param interceptor 链 |
| | | * @param inner 拦截器 |
| | | * @param index 位置 |
| | | */ |
| | | public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) { |
| | | List<InnerInterceptor> inners = new ArrayList<>(interceptor.getInterceptors()); |
| | | inners.add(index, inner); |
| | | interceptor.setInterceptors(inners); |
| | | } |
| | | |
| | | /** |
| | | * 获得 Table 对应的表名 |
| | | * <p> |
| | | * 兼容 MySQL 转义表名 `t_xxx` |
| | | * |
| | | * @param table 表 |
| | | * @return 去除转移字符后的表名 |
| | | */ |
| | | public static String getTableName(Table table) { |
| | | String tableName = table.getName(); |
| | | if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) { |
| | | tableName = tableName.substring(1, tableName.length() - 1); |
| | | } |
| | | return tableName; |
| | | } |
| | | |
| | | /** |
| | | * 构建 Column 对象 |
| | | * |
| | | * @param tableName 表名 |
| | | * @param tableAlias 别名 |
| | | * @param column 字段名 |
| | | * @return Column 对象 |
| | | */ |
| | | public static Column buildColumn(String tableName, Alias tableAlias, String column) { |
| | | if (tableAlias != null) { |
| | | tableName = tableAlias.getName(); |
| | | } |
| | | return new Column(tableName + StringPool.DOT + column); |
| | | } |
| | | |
| | | /** |
| | | * 跨数据库的 find_in_set 实现 |
| | | * |
| | | * @param column 字段名称 |
| | | * @param value 查询值(不带单引号) |
| | | * @return sql |
| | | */ |
| | | public static String findInSet(String column, Object value) { |
| | | // 这里不用SqlConstants.DB_TYPE,因为它是使用 primary 数据源的 url 推断出来的类型 |
| | | DbType dbType = JdbcUtils.getDbType(); |
| | | return DbTypeEnum.getFindInSetTemplate(dbType) |
| | | .replace("#{column}", column) |
| | | .replace("#{value}", StrUtil.toString(value)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * |
| | | * https://www.renren.io |
| | | * |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.mybatis.interceptor; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.baomidou.mybatisplus.core.toolkit.PluginUtils; |
| | | import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; |
| | | import net.sf.jsqlparser.JSQLParserException; |
| | | import net.sf.jsqlparser.expression.Expression; |
| | | import net.sf.jsqlparser.expression.StringValue; |
| | | import net.sf.jsqlparser.expression.operators.conditional.AndExpression; |
| | | import net.sf.jsqlparser.parser.CCJSqlParserUtil; |
| | | import net.sf.jsqlparser.statement.select.PlainSelect; |
| | | import net.sf.jsqlparser.statement.select.Select; |
| | | import org.apache.ibatis.executor.Executor; |
| | | import org.apache.ibatis.mapping.BoundSql; |
| | | import org.apache.ibatis.mapping.MappedStatement; |
| | | import org.apache.ibatis.session.ResultHandler; |
| | | import org.apache.ibatis.session.RowBounds; |
| | | |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 数据过滤 |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | */ |
| | | public class DataFilterInterceptor implements InnerInterceptor { |
| | | |
| | | @Override |
| | | public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { |
| | | DataScope scope = getDataScope(parameter); |
| | | // 不进行数据过滤 |
| | | if(scope == null || StrUtil.isBlank(scope.getSqlFilter())){ |
| | | return; |
| | | } |
| | | |
| | | // 拼接新SQL |
| | | String buildSql = getSelect(boundSql.getSql(), scope); |
| | | |
| | | // 重写SQL |
| | | PluginUtils.mpBoundSql(boundSql).sql(buildSql); |
| | | } |
| | | |
| | | private DataScope getDataScope(Object parameter){ |
| | | if (parameter == null){ |
| | | return null; |
| | | } |
| | | |
| | | // 判断参数里是否有DataScope对象 |
| | | if (parameter instanceof Map) { |
| | | Map<?, ?> parameterMap = (Map<?, ?>) parameter; |
| | | for (Map.Entry entry : parameterMap.entrySet()) { |
| | | if (entry.getValue() != null && entry.getValue() instanceof DataScope) { |
| | | return (DataScope) entry.getValue(); |
| | | } |
| | | } |
| | | } else if (parameter instanceof DataScope) { |
| | | return (DataScope) parameter; |
| | | } |
| | | |
| | | return null; |
| | | } |
| | | |
| | | private String getSelect(String buildSql, DataScope scope){ |
| | | try { |
| | | Select select = (Select) CCJSqlParserUtil.parse(buildSql); |
| | | PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); |
| | | |
| | | Expression expression = plainSelect.getWhere(); |
| | | if(expression == null){ |
| | | plainSelect.setWhere(new StringValue(scope.getSqlFilter())); |
| | | }else{ |
| | | AndExpression andExpression = new AndExpression(expression, new StringValue(scope.getSqlFilter())); |
| | | plainSelect.setWhere(andExpression); |
| | | } |
| | | |
| | | return select.toString().replaceAll("'", ""); |
| | | }catch (JSQLParserException e){ |
| | | return buildSql; |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * |
| | | * https://www.renren.io |
| | | * |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.mybatis.interceptor; |
| | | |
| | | /** |
| | | * 数据范围 |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | * @since 1.0.0 |
| | | */ |
| | | public class DataScope { |
| | | private String sqlFilter; |
| | | |
| | | public DataScope(String sqlFilter) { |
| | | this.sqlFilter = sqlFilter; |
| | | } |
| | | |
| | | public String getSqlFilter() { |
| | | return sqlFilter; |
| | | } |
| | | |
| | | public void setSqlFilter(String sqlFilter) { |
| | | this.sqlFilter = sqlFilter; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return this.sqlFilter; |
| | | } |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 使用 MyBatis Plus 提升使用 MyBatis 的开发效率 |
| | | */ |
| | | package com.iailab.framework.mybatis; |
对比新文件 |
| | |
| | | package com.iailab.framework.translate.config; |
| | | |
| | | import com.iailab.framework.translate.core.TranslateUtils; |
| | | import com.fhs.trans.service.impl.TransService; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | @AutoConfiguration |
| | | public class IailabTranslateAutoConfiguration { |
| | | |
| | | @Bean |
| | | @SuppressWarnings({"InstantiationOfUtilityClass", "SpringJavaInjectionPointsAutowiringInspection"}) |
| | | public TranslateUtils translateUtils(TransService transService) { |
| | | TranslateUtils.init(transService); |
| | | return new TranslateUtils(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.translate.core; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.fhs.core.trans.vo.VO; |
| | | import com.fhs.trans.service.impl.TransService; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * VO 数据翻译 Utils |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TranslateUtils { |
| | | |
| | | private static TransService transService; |
| | | |
| | | public static void init(TransService transService) { |
| | | TranslateUtils.transService = transService; |
| | | } |
| | | |
| | | /** |
| | | * 数据翻译 |
| | | * |
| | | * 使用场景:无法使用 @TransMethodResult 注解的场景,只能通过手动触发翻译 |
| | | * |
| | | * @param data 数据 |
| | | * @return 翻译结果 |
| | | */ |
| | | public static <T extends VO> List<T> translate(List<T> data) { |
| | | if (CollUtil.isNotEmpty((data))) { |
| | | transService.transBatch(data); |
| | | } |
| | | return data; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 使用 Easy-Trans 提升使用 VO 数据翻译的开发效率 |
| | | */ |
| | | package com.iailab.framework.translate; |
对比新文件 |
| | |
| | | org.springframework.boot.env.EnvironmentPostProcessor=\ |
| | | com.iailab.framework.mybatis.config.IdTypeEnvironmentPostProcessor |
对比新文件 |
| | |
| | | com.iailab.framework.datasource.config.IailabDataSourceAutoConfiguration |
| | | com.iailab.framework.mybatis.config.IailabMybatisAutoConfiguration |
| | | com.iailab.framework.translate.config.IailabTranslateAutoConfiguration |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-protection</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>服务保证,提供分布式锁、幂等、限流、熔断等等功能</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <!-- Web 相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-web</artifactId> |
| | | <scope>provided</scope> <!-- 设置为 provided,只有限流、幂等使用到 --> |
| | | </dependency> |
| | | |
| | | <!-- DB 相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-redis</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- 服务保障相关 --> |
| | | <dependency> |
| | | <groupId>com.baomidou</groupId> |
| | | <artifactId>lock4j-redisson-spring-boot-starter</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <!-- Test 测试相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-test</artifactId> |
| | | <scope>test</scope> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.idempotent.config; |
| | | |
| | | import com.iailab.framework.idempotent.core.aop.IdempotentAspect; |
| | | import com.iailab.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; |
| | | import com.iailab.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; |
| | | import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| | | import com.iailab.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver; |
| | | import com.iailab.framework.idempotent.core.redis.IdempotentRedisDAO; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import com.iailab.framework.redis.config.IailabRedisAutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.data.redis.core.StringRedisTemplate; |
| | | |
| | | import java.util.List; |
| | | |
| | | @AutoConfiguration(after = IailabRedisAutoConfiguration.class) |
| | | public class IailabIdempotentConfiguration { |
| | | |
| | | @Bean |
| | | public IdempotentAspect idempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { |
| | | return new IdempotentAspect(keyResolvers, idempotentRedisDAO); |
| | | } |
| | | |
| | | @Bean |
| | | public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) { |
| | | return new IdempotentRedisDAO(stringRedisTemplate); |
| | | } |
| | | |
| | | // ========== 各种 IdempotentKeyResolver Bean ========== |
| | | |
| | | @Bean |
| | | public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() { |
| | | return new DefaultIdempotentKeyResolver(); |
| | | } |
| | | |
| | | @Bean |
| | | public UserIdempotentKeyResolver userIdempotentKeyResolver() { |
| | | return new UserIdempotentKeyResolver(); |
| | | } |
| | | |
| | | @Bean |
| | | public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() { |
| | | return new ExpressionIdempotentKeyResolver(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.idempotent.core.annotation; |
| | | |
| | | import com.iailab.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; |
| | | import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| | | import com.iailab.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; |
| | | import com.iailab.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver; |
| | | |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | | * 幂等注解 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Target({ElementType.METHOD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | public @interface Idempotent { |
| | | |
| | | /** |
| | | * 幂等的超时时间,默认为 1 秒 |
| | | * |
| | | * 注意,如果执行时间超过它,请求还是会进来 |
| | | */ |
| | | int timeout() default 1; |
| | | /** |
| | | * 时间单位,默认为 SECONDS 秒 |
| | | */ |
| | | TimeUnit timeUnit() default TimeUnit.SECONDS; |
| | | |
| | | /** |
| | | * 提示信息,正在执行中的提示 |
| | | */ |
| | | String message() default "重复请求,请稍后重试"; |
| | | |
| | | /** |
| | | * 使用的 Key 解析器 |
| | | * |
| | | * @see DefaultIdempotentKeyResolver 全局级别 |
| | | * @see UserIdempotentKeyResolver 用户级别 |
| | | * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算 |
| | | */ |
| | | Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class; |
| | | /** |
| | | * 使用的 Key 参数 |
| | | */ |
| | | String keyArg() default ""; |
| | | |
| | | /** |
| | | * 删除 Key,当发生异常时候 |
| | | * |
| | | * 问题:为什么发生异常时,需要删除 Key 呢? |
| | | * 回答:发生异常时,说明业务发生错误,此时需要删除 Key,避免下次请求无法正常执行。 |
| | | * |
| | | * 问题:为什么不搞 deleteWhenSuccess 执行成功时,需要删除 Key 呢? |
| | | * 回答:这种情况下,本质上是分布式锁,推荐使用 @Lock4j 注解 |
| | | */ |
| | | boolean deleteKeyWhenException() default true; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.idempotent.core.aop; |
| | | |
| | | import com.iailab.framework.common.exception.ServiceException; |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import com.iailab.framework.common.util.collection.CollectionUtils; |
| | | import com.iailab.framework.idempotent.core.annotation.Idempotent; |
| | | import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| | | import com.iailab.framework.idempotent.core.redis.IdempotentRedisDAO; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.aspectj.lang.ProceedingJoinPoint; |
| | | import org.aspectj.lang.annotation.Around; |
| | | import org.aspectj.lang.annotation.Aspect; |
| | | import org.springframework.util.Assert; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Aspect |
| | | @Slf4j |
| | | public class IdempotentAspect { |
| | | |
| | | /** |
| | | * IdempotentKeyResolver 集合 |
| | | */ |
| | | private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers; |
| | | |
| | | private final IdempotentRedisDAO idempotentRedisDAO; |
| | | |
| | | public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { |
| | | this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass); |
| | | this.idempotentRedisDAO = idempotentRedisDAO; |
| | | } |
| | | |
| | | @Around(value = "@annotation(idempotent)") |
| | | public Object aroundPointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { |
| | | // 获得 IdempotentKeyResolver |
| | | IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); |
| | | Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); |
| | | // 解析 Key |
| | | String key = keyResolver.resolver(joinPoint, idempotent); |
| | | |
| | | // 1. 锁定 Key |
| | | boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); |
| | | // 锁定失败,抛出异常 |
| | | if (!success) { |
| | | log.info("[aroundPointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); |
| | | throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message()); |
| | | } |
| | | |
| | | // 2. 执行逻辑 |
| | | try { |
| | | return joinPoint.proceed(); |
| | | } catch (Throwable throwable) { |
| | | // 3. 异常时,删除 Key |
| | | // 参考美团 GTIS 思路:https://tech.meituan.com/2016/09/29/distributed-system-mutually-exclusive-idempotence-cerberus-gtis.html |
| | | if (idempotent.deleteKeyWhenException()) { |
| | | idempotentRedisDAO.delete(key); |
| | | } |
| | | throw throwable; |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.idempotent.core.keyresolver; |
| | | |
| | | import com.iailab.framework.idempotent.core.annotation.Idempotent; |
| | | import org.aspectj.lang.JoinPoint; |
| | | |
| | | /** |
| | | * 幂等 Key 解析器接口 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface IdempotentKeyResolver { |
| | | |
| | | /** |
| | | * 解析一个 Key |
| | | * |
| | | * @param idempotent 幂等注解 |
| | | * @param joinPoint AOP 切面 |
| | | * @return Key |
| | | */ |
| | | String resolver(JoinPoint joinPoint, Idempotent idempotent); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.idempotent.core.keyresolver.impl; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.crypto.SecureUtil; |
| | | import com.iailab.framework.idempotent.core.annotation.Idempotent; |
| | | import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| | | import org.aspectj.lang.JoinPoint; |
| | | |
| | | /** |
| | | * 默认(全局级别)幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key |
| | | * |
| | | * 为了避免 Key 过长,使用 MD5 进行“压缩” |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver { |
| | | |
| | | @Override |
| | | public String resolver(JoinPoint joinPoint, Idempotent idempotent) { |
| | | String methodName = joinPoint.getSignature().toString(); |
| | | String argsStr = StrUtil.join(",", joinPoint.getArgs()); |
| | | return SecureUtil.md5(methodName + argsStr); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.idempotent.core.keyresolver.impl; |
| | | |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import com.iailab.framework.idempotent.core.annotation.Idempotent; |
| | | import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| | | import org.aspectj.lang.JoinPoint; |
| | | import org.aspectj.lang.reflect.MethodSignature; |
| | | import org.springframework.core.LocalVariableTableParameterNameDiscoverer; |
| | | import org.springframework.core.ParameterNameDiscoverer; |
| | | import org.springframework.expression.Expression; |
| | | import org.springframework.expression.ExpressionParser; |
| | | import org.springframework.expression.spel.standard.SpelExpressionParser; |
| | | import org.springframework.expression.spel.support.StandardEvaluationContext; |
| | | |
| | | import java.lang.reflect.Method; |
| | | |
| | | /** |
| | | * 基于 Spring EL 表达式, |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver { |
| | | |
| | | private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); |
| | | private final ExpressionParser expressionParser = new SpelExpressionParser(); |
| | | |
| | | @Override |
| | | public String resolver(JoinPoint joinPoint, Idempotent idempotent) { |
| | | // 获得被拦截方法参数名列表 |
| | | Method method = getMethod(joinPoint); |
| | | Object[] args = joinPoint.getArgs(); |
| | | String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method); |
| | | // 准备 Spring EL 表达式解析的上下文 |
| | | StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); |
| | | if (ArrayUtil.isNotEmpty(parameterNames)) { |
| | | for (int i = 0; i < parameterNames.length; i++) { |
| | | evaluationContext.setVariable(parameterNames[i], args[i]); |
| | | } |
| | | } |
| | | |
| | | // 解析参数 |
| | | Expression expression = expressionParser.parseExpression(idempotent.keyArg()); |
| | | return expression.getValue(evaluationContext, String.class); |
| | | } |
| | | |
| | | private static Method getMethod(JoinPoint point) { |
| | | // 处理,声明在类上的情况 |
| | | MethodSignature signature = (MethodSignature) point.getSignature(); |
| | | Method method = signature.getMethod(); |
| | | if (!method.getDeclaringClass().isInterface()) { |
| | | return method; |
| | | } |
| | | |
| | | // 处理,声明在接口上的情况 |
| | | try { |
| | | return point.getTarget().getClass().getDeclaredMethod( |
| | | point.getSignature().getName(), method.getParameterTypes()); |
| | | } catch (NoSuchMethodException e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.idempotent.core.keyresolver.impl; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.crypto.SecureUtil; |
| | | import com.iailab.framework.idempotent.core.annotation.Idempotent; |
| | | import com.iailab.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import org.aspectj.lang.JoinPoint; |
| | | |
| | | /** |
| | | * 用户级别的幂等 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key |
| | | * |
| | | * 为了避免 Key 过长,使用 MD5 进行“压缩” |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class UserIdempotentKeyResolver implements IdempotentKeyResolver { |
| | | |
| | | @Override |
| | | public String resolver(JoinPoint joinPoint, Idempotent idempotent) { |
| | | String methodName = joinPoint.getSignature().toString(); |
| | | String argsStr = StrUtil.join(",", joinPoint.getArgs()); |
| | | Long userId = WebFrameworkUtils.getLoginUserId(); |
| | | Integer userType = WebFrameworkUtils.getLoginUserType(); |
| | | return SecureUtil.md5(methodName + argsStr + userId + userType); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.idempotent.core.redis; |
| | | |
| | | import lombok.AllArgsConstructor; |
| | | import org.springframework.data.redis.core.StringRedisTemplate; |
| | | |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | | * 幂等 Redis DAO |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AllArgsConstructor |
| | | public class IdempotentRedisDAO { |
| | | |
| | | /** |
| | | * 幂等操作 |
| | | * |
| | | * KEY 格式:idempotent:%s // 参数为 uuid |
| | | * VALUE 格式:String |
| | | * 过期时间:不固定 |
| | | */ |
| | | private static final String IDEMPOTENT = "idempotent:%s"; |
| | | |
| | | private final StringRedisTemplate redisTemplate; |
| | | |
| | | public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) { |
| | | String redisKey = formatKey(key); |
| | | return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit); |
| | | } |
| | | |
| | | public void delete(String key) { |
| | | String redisKey = formatKey(key); |
| | | redisTemplate.delete(redisKey); |
| | | } |
| | | |
| | | private static String formatKey(String key) { |
| | | return String.format(IDEMPOTENT, key); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现 |
| | | * 实现原理是,相同参数的方法,一段时间内,有且仅能执行一次。通过这样的方式,保证幂等性。 |
| | | * |
| | | * 使用场景:例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。 |
| | | * |
| | | * 和 it4alla/idempotent 组件的差异点,主要体现在两点: |
| | | * 1. 我们去掉了 @Idempotent 注解的 delKey 属性。原因是,本质上 delKey 为 true 时,实现的是分布式锁的能力 |
| | | * 此时,我们偏向使用 Lock4j 组件。原则上,一个组件只提供一种单一的能力。 |
| | | * 2. 考虑到组件的通用性,我们并未像 it4alla/idempotent 组件一样使用 Redisson RMap 结构,而是直接使用 Redis 的 String 数据格式。 |
| | | */ |
| | | package com.iailab.framework.idempotent; |
对比新文件 |
| | |
| | | package com.iailab.framework.lock4j.config; |
| | | |
| | | import com.iailab.framework.lock4j.core.DefaultLockFailureStrategy; |
| | | import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | @AutoConfiguration(before = LockAutoConfiguration.class) |
| | | @ConditionalOnClass(name = "com.baomidou.lock.annotation.Lock4j") |
| | | public class IailabLock4jConfiguration { |
| | | |
| | | @Bean |
| | | public DefaultLockFailureStrategy lockFailureStrategy() { |
| | | return new DefaultLockFailureStrategy(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.lock4j.core; |
| | | |
| | | import com.iailab.framework.common.exception.ServiceException; |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import com.baomidou.lock.LockFailureStrategy; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.lang.reflect.Method; |
| | | |
| | | /** |
| | | * 自定义获取锁失败策略,抛出 {@link ServiceException} 异常 |
| | | */ |
| | | @Slf4j |
| | | public class DefaultLockFailureStrategy implements LockFailureStrategy { |
| | | |
| | | @Override |
| | | public void onLockFailure(String key, Method method, Object[] arguments) { |
| | | log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取失败:{} ]", Thread.currentThread().getName(), key, arguments); |
| | | throw new ServiceException(GlobalErrorCodeConstants.LOCKED); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.lock4j.core; |
| | | |
| | | /** |
| | | * Lock4j Redis Key 枚举类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface Lock4jRedisKeyConstants { |
| | | |
| | | /** |
| | | * 分布式锁 |
| | | * |
| | | * KEY 格式:lock4j:%s // 参数来自 DefaultLockKeyBuilder 类 |
| | | * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构 |
| | | * 过期时间:不固定 |
| | | */ |
| | | String LOCK4J = "lock4j:%s"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目 |
| | | */ |
| | | package com.iailab.framework.lock4j; |
对比新文件 |
| | |
| | | package com.iailab.framework.ratelimiter.config; |
| | | |
| | | import com.iailab.framework.ratelimiter.core.aop.RateLimiterAspect; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.impl.*; |
| | | import com.iailab.framework.ratelimiter.core.redis.RateLimiterRedisDAO; |
| | | import com.iailab.framework.redis.config.IailabRedisAutoConfiguration; |
| | | import org.redisson.api.RedissonClient; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | import java.util.List; |
| | | |
| | | @AutoConfiguration(after = IailabRedisAutoConfiguration.class) |
| | | public class IailabRateLimiterConfiguration { |
| | | |
| | | @Bean |
| | | public RateLimiterAspect rateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) { |
| | | return new RateLimiterAspect(keyResolvers, rateLimiterRedisDAO); |
| | | } |
| | | |
| | | @Bean |
| | | @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") |
| | | public RateLimiterRedisDAO rateLimiterRedisDAO(RedissonClient redissonClient) { |
| | | return new RateLimiterRedisDAO(redissonClient); |
| | | } |
| | | |
| | | // ========== 各种 RateLimiterRedisDAO Bean ========== |
| | | |
| | | @Bean |
| | | public DefaultRateLimiterKeyResolver defaultRateLimiterKeyResolver() { |
| | | return new DefaultRateLimiterKeyResolver(); |
| | | } |
| | | |
| | | @Bean |
| | | public UserRateLimiterKeyResolver userRateLimiterKeyResolver() { |
| | | return new UserRateLimiterKeyResolver(); |
| | | } |
| | | |
| | | @Bean |
| | | public ClientIpRateLimiterKeyResolver clientIpRateLimiterKeyResolver() { |
| | | return new ClientIpRateLimiterKeyResolver(); |
| | | } |
| | | |
| | | @Bean |
| | | public ServerNodeRateLimiterKeyResolver serverNodeRateLimiterKeyResolver() { |
| | | return new ServerNodeRateLimiterKeyResolver(); |
| | | } |
| | | |
| | | @Bean |
| | | public ExpressionRateLimiterKeyResolver expressionRateLimiterKeyResolver() { |
| | | return new ExpressionRateLimiterKeyResolver(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ratelimiter.core.annotation; |
| | | |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import com.iailab.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.impl.ClientIpRateLimiterKeyResolver; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.impl.DefaultRateLimiterKeyResolver; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.impl.ServerNodeRateLimiterKeyResolver; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.impl.UserRateLimiterKeyResolver; |
| | | |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | | * 限流注解 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Target({ElementType.METHOD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | public @interface RateLimiter { |
| | | |
| | | /** |
| | | * 限流的时间,默认为 1 秒 |
| | | */ |
| | | int time() default 1; |
| | | /** |
| | | * 时间单位,默认为 SECONDS 秒 |
| | | */ |
| | | TimeUnit timeUnit() default TimeUnit.SECONDS; |
| | | |
| | | /** |
| | | * 限流次数 |
| | | */ |
| | | int count() default 100; |
| | | |
| | | /** |
| | | * 提示信息,请求过快的提示 |
| | | * |
| | | * @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS |
| | | */ |
| | | String message() default ""; // 为空时,使用 TOO_MANY_REQUESTS 错误提示 |
| | | |
| | | /** |
| | | * 使用的 Key 解析器 |
| | | * |
| | | * @see DefaultRateLimiterKeyResolver 全局级别 |
| | | * @see UserRateLimiterKeyResolver 用户 ID 级别 |
| | | * @see ClientIpRateLimiterKeyResolver 用户 IP 级别 |
| | | * @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别 |
| | | * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算 |
| | | */ |
| | | Class<? extends RateLimiterKeyResolver> keyResolver() default DefaultRateLimiterKeyResolver.class; |
| | | /** |
| | | * 使用的 Key 参数 |
| | | */ |
| | | String keyArg() default ""; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ratelimiter.core.aop; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.exception.ServiceException; |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import com.iailab.framework.common.util.collection.CollectionUtils; |
| | | import com.iailab.framework.ratelimiter.core.annotation.RateLimiter; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; |
| | | import com.iailab.framework.ratelimiter.core.redis.RateLimiterRedisDAO; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.aspectj.lang.JoinPoint; |
| | | import org.aspectj.lang.annotation.Aspect; |
| | | import org.aspectj.lang.annotation.Before; |
| | | import org.springframework.util.Assert; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 拦截声明了 {@link RateLimiter} 注解的方法,实现限流操作 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Aspect |
| | | @Slf4j |
| | | public class RateLimiterAspect { |
| | | |
| | | /** |
| | | * RateLimiterKeyResolver 集合 |
| | | */ |
| | | private final Map<Class<? extends RateLimiterKeyResolver>, RateLimiterKeyResolver> keyResolvers; |
| | | |
| | | private final RateLimiterRedisDAO rateLimiterRedisDAO; |
| | | |
| | | public RateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) { |
| | | this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass); |
| | | this.rateLimiterRedisDAO = rateLimiterRedisDAO; |
| | | } |
| | | |
| | | @Before("@annotation(rateLimiter)") |
| | | public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) { |
| | | // 获得 IdempotentKeyResolver 对象 |
| | | RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver()); |
| | | Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver"); |
| | | // 解析 Key |
| | | String key = keyResolver.resolver(joinPoint, rateLimiter); |
| | | |
| | | // 获取 1 次限流 |
| | | boolean success = rateLimiterRedisDAO.tryAcquire(key, |
| | | rateLimiter.count(), rateLimiter.time(), rateLimiter.timeUnit()); |
| | | if (!success) { |
| | | log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]", joinPoint.getSignature().toString(), joinPoint.getArgs()); |
| | | String message = StrUtil.blankToDefault(rateLimiter.message(), |
| | | GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg()); |
| | | throw new ServiceException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(), message); |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
对比新文件 |
| | |
| | | package com.iailab.framework.ratelimiter.core.keyresolver; |
| | | |
| | | import com.iailab.framework.ratelimiter.core.annotation.RateLimiter; |
| | | import org.aspectj.lang.JoinPoint; |
| | | |
| | | /** |
| | | * 限流 Key 解析器接口 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface RateLimiterKeyResolver { |
| | | |
| | | /** |
| | | * 解析一个 Key |
| | | * |
| | | * @param rateLimiter 限流注解 |
| | | * @param joinPoint AOP 切面 |
| | | * @return Key |
| | | */ |
| | | String resolver(JoinPoint joinPoint, RateLimiter rateLimiter); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ratelimiter.core.keyresolver.impl; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.crypto.SecureUtil; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import com.iailab.framework.ratelimiter.core.annotation.RateLimiter; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; |
| | | import org.aspectj.lang.JoinPoint; |
| | | |
| | | /** |
| | | * IP 级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key |
| | | * |
| | | * 为了避免 Key 过长,使用 MD5 进行“压缩” |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver { |
| | | |
| | | @Override |
| | | public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { |
| | | String methodName = joinPoint.getSignature().toString(); |
| | | String argsStr = StrUtil.join(",", joinPoint.getArgs()); |
| | | String clientIp = ServletUtils.getClientIP(); |
| | | return SecureUtil.md5(methodName + argsStr + clientIp); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ratelimiter.core.keyresolver.impl; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.crypto.SecureUtil; |
| | | import com.iailab.framework.ratelimiter.core.annotation.RateLimiter; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; |
| | | import org.aspectj.lang.JoinPoint; |
| | | |
| | | /** |
| | | * 默认(全局级别)限流 Key 解析器,使用方法名 + 方法参数,组装成一个 Key |
| | | * |
| | | * 为了避免 Key 过长,使用 MD5 进行“压缩” |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver { |
| | | |
| | | @Override |
| | | public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { |
| | | String methodName = joinPoint.getSignature().toString(); |
| | | String argsStr = StrUtil.join(",", joinPoint.getArgs()); |
| | | return SecureUtil.md5(methodName + argsStr); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ratelimiter.core.keyresolver.impl; |
| | | |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import com.iailab.framework.ratelimiter.core.annotation.RateLimiter; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; |
| | | import org.aspectj.lang.JoinPoint; |
| | | import org.aspectj.lang.reflect.MethodSignature; |
| | | import org.springframework.core.DefaultParameterNameDiscoverer; |
| | | import org.springframework.core.ParameterNameDiscoverer; |
| | | import org.springframework.expression.Expression; |
| | | import org.springframework.expression.ExpressionParser; |
| | | import org.springframework.expression.spel.standard.SpelExpressionParser; |
| | | import org.springframework.expression.spel.support.StandardEvaluationContext; |
| | | |
| | | import java.lang.reflect.Method; |
| | | |
| | | /** |
| | | * 基于 Spring EL 表达式的 {@link RateLimiterKeyResolver} 实现类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class ExpressionRateLimiterKeyResolver implements RateLimiterKeyResolver { |
| | | |
| | | private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); |
| | | |
| | | private final ExpressionParser expressionParser = new SpelExpressionParser(); |
| | | |
| | | @Override |
| | | public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { |
| | | // 获得被拦截方法参数名列表 |
| | | Method method = getMethod(joinPoint); |
| | | Object[] args = joinPoint.getArgs(); |
| | | String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method); |
| | | // 准备 Spring EL 表达式解析的上下文 |
| | | StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); |
| | | if (ArrayUtil.isNotEmpty(parameterNames)) { |
| | | for (int i = 0; i < parameterNames.length; i++) { |
| | | evaluationContext.setVariable(parameterNames[i], args[i]); |
| | | } |
| | | } |
| | | |
| | | // 解析参数 |
| | | Expression expression = expressionParser.parseExpression(rateLimiter.keyArg()); |
| | | return expression.getValue(evaluationContext, String.class); |
| | | } |
| | | |
| | | private static Method getMethod(JoinPoint point) { |
| | | // 处理,声明在类上的情况 |
| | | MethodSignature signature = (MethodSignature) point.getSignature(); |
| | | Method method = signature.getMethod(); |
| | | if (!method.getDeclaringClass().isInterface()) { |
| | | return method; |
| | | } |
| | | |
| | | // 处理,声明在接口上的情况 |
| | | try { |
| | | return point.getTarget().getClass().getDeclaredMethod( |
| | | point.getSignature().getName(), method.getParameterTypes()); |
| | | } catch (NoSuchMethodException e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ratelimiter.core.keyresolver.impl; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.crypto.SecureUtil; |
| | | import cn.hutool.system.SystemUtil; |
| | | import com.iailab.framework.ratelimiter.core.annotation.RateLimiter; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; |
| | | import org.aspectj.lang.JoinPoint; |
| | | |
| | | /** |
| | | * Server 节点级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key |
| | | * |
| | | * 为了避免 Key 过长,使用 MD5 进行“压缩” |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class ServerNodeRateLimiterKeyResolver implements RateLimiterKeyResolver { |
| | | |
| | | @Override |
| | | public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { |
| | | String methodName = joinPoint.getSignature().toString(); |
| | | String argsStr = StrUtil.join(",", joinPoint.getArgs()); |
| | | String serverNode = String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); |
| | | return SecureUtil.md5(methodName + argsStr + serverNode); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ratelimiter.core.keyresolver.impl; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.crypto.SecureUtil; |
| | | import com.iailab.framework.ratelimiter.core.annotation.RateLimiter; |
| | | import com.iailab.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import org.aspectj.lang.JoinPoint; |
| | | |
| | | /** |
| | | * 用户级别的限流 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key |
| | | * |
| | | * 为了避免 Key 过长,使用 MD5 进行“压缩” |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver { |
| | | |
| | | @Override |
| | | public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { |
| | | String methodName = joinPoint.getSignature().toString(); |
| | | String argsStr = StrUtil.join(",", joinPoint.getArgs()); |
| | | Long userId = WebFrameworkUtils.getLoginUserId(); |
| | | Integer userType = WebFrameworkUtils.getLoginUserType(); |
| | | return SecureUtil.md5(methodName + argsStr + userId + userType); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ratelimiter.core.redis; |
| | | |
| | | import lombok.AllArgsConstructor; |
| | | import org.redisson.api.*; |
| | | |
| | | import java.util.Objects; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | | * 限流 Redis DAO |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AllArgsConstructor |
| | | public class RateLimiterRedisDAO { |
| | | |
| | | /** |
| | | * 限流操作 |
| | | * |
| | | * KEY 格式:rate_limiter:%s // 参数为 uuid |
| | | * VALUE 格式:String |
| | | * 过期时间:不固定 |
| | | */ |
| | | private static final String RATE_LIMITER = "rate_limiter:%s"; |
| | | |
| | | private final RedissonClient redissonClient; |
| | | |
| | | public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) { |
| | | // 1. 获得 RRateLimiter,并设置 rate 速率 |
| | | RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit); |
| | | // 2. 尝试获取 1 个 |
| | | return rateLimiter.tryAcquire(); |
| | | } |
| | | |
| | | private static String formatKey(String key) { |
| | | return String.format(RATE_LIMITER, key); |
| | | } |
| | | |
| | | private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) { |
| | | String redisKey = formatKey(key); |
| | | RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey); |
| | | long rateInterval = timeUnit.toSeconds(time); |
| | | // 1. 如果不存在,设置 rate 速率 |
| | | RateLimiterConfig config = rateLimiter.getConfig(); |
| | | if (config == null) { |
| | | rateLimiter.trySetRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS); |
| | | return rateLimiter; |
| | | } |
| | | // 2. 如果存在,并且配置相同,则直接返回 |
| | | if (config.getRateType() == RateType.OVERALL |
| | | && Objects.equals(config.getRate(), count) |
| | | && Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) { |
| | | return rateLimiter; |
| | | } |
| | | // 3. 如果存在,并且配置不同,则进行新建 |
| | | rateLimiter.setRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS); |
| | | return rateLimiter; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 限流组件,基于 Redisson {@link org.redisson.api.RRateLimiter} 限流实现 |
| | | */ |
| | | package com.iailab.framework.ratelimiter; |
对比新文件 |
| | |
| | | package com.iailab.framework.signature.config; |
| | | |
| | | import com.iailab.framework.redis.config.IailabRedisAutoConfiguration; |
| | | import com.iailab.framework.signature.core.aop.ApiSignatureAspect; |
| | | import com.iailab.framework.signature.core.redis.ApiSignatureRedisDAO; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.data.redis.core.StringRedisTemplate; |
| | | |
| | | /** |
| | | * HTTP API 签名的自动配置类 |
| | | * |
| | | * @author Zhougang |
| | | */ |
| | | @AutoConfiguration(after = IailabRedisAutoConfiguration.class) |
| | | public class IailabApiSignatureAutoConfiguration { |
| | | |
| | | @Bean |
| | | public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) { |
| | | return new ApiSignatureAspect(signatureRedisDAO); |
| | | } |
| | | |
| | | @Bean |
| | | public ApiSignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) { |
| | | return new ApiSignatureRedisDAO(stringRedisTemplate); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.signature.core.annotation; |
| | | |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | |
| | | import java.lang.annotation.*; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | |
| | | /** |
| | | * HTTP API 签名注解 |
| | | * |
| | | * @author Zhougang |
| | | */ |
| | | @Inherited |
| | | @Documented |
| | | @Target({ElementType.METHOD, ElementType.TYPE}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | public @interface ApiSignature { |
| | | |
| | | /** |
| | | * 同一个请求多长时间内有效 默认 60 秒 |
| | | */ |
| | | int timeout() default 60; |
| | | |
| | | /** |
| | | * 时间单位,默认为 SECONDS 秒 |
| | | */ |
| | | TimeUnit timeUnit() default TimeUnit.SECONDS; |
| | | |
| | | // ========================== 签名参数 ========================== |
| | | |
| | | /** |
| | | * 提示信息,签名失败的提示 |
| | | * |
| | | * @see GlobalErrorCodeConstants#BAD_REQUEST |
| | | */ |
| | | String message() default "签名不正确"; // 为空时,使用 BAD_REQUEST 错误提示 |
| | | |
| | | /** |
| | | * 签名字段:appId 应用ID |
| | | */ |
| | | String appId() default "appId"; |
| | | |
| | | /** |
| | | * 签名字段:timestamp 时间戳 |
| | | */ |
| | | String timestamp() default "timestamp"; |
| | | |
| | | /** |
| | | * 签名字段:nonce 随机数,10 位以上 |
| | | */ |
| | | String nonce() default "nonce"; |
| | | |
| | | /** |
| | | * sign 客户端签名 |
| | | */ |
| | | String sign() default "sign"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.signature.core.aop; |
| | | |
| | | import cn.hutool.core.lang.Assert; |
| | | import cn.hutool.core.map.MapUtil; |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.crypto.digest.DigestUtil; |
| | | import com.iailab.framework.common.exception.ServiceException; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import com.iailab.framework.signature.core.annotation.ApiSignature; |
| | | import com.iailab.framework.signature.core.redis.ApiSignatureRedisDAO; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.aspectj.lang.JoinPoint; |
| | | import org.aspectj.lang.annotation.Aspect; |
| | | import org.aspectj.lang.annotation.Before; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.SortedMap; |
| | | import java.util.TreeMap; |
| | | |
| | | import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; |
| | | |
| | | /** |
| | | * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名 |
| | | * |
| | | * @author Zhougang |
| | | */ |
| | | @Aspect |
| | | @Slf4j |
| | | @AllArgsConstructor |
| | | public class ApiSignatureAspect { |
| | | |
| | | private final ApiSignatureRedisDAO signatureRedisDAO; |
| | | |
| | | @Before("@annotation(signature)") |
| | | public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) { |
| | | // 1. 验证通过,直接结束 |
| | | if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) { |
| | | return; |
| | | } |
| | | |
| | | // 2. 验证不通过,抛出异常 |
| | | log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(), |
| | | joinPoint.getArgs()); |
| | | throw new ServiceException(BAD_REQUEST.getCode(), |
| | | StrUtil.blankToDefault(signature.message(), BAD_REQUEST.getMsg())); |
| | | } |
| | | |
| | | public boolean verifySignature(ApiSignature signature, HttpServletRequest request) { |
| | | // 1.1 校验 Header |
| | | if (!verifyHeaders(signature, request)) { |
| | | return false; |
| | | } |
| | | // 1.2 校验 appId 是否能获取到对应的 appSecret |
| | | String appId = request.getHeader(signature.appId()); |
| | | String appSecret = signatureRedisDAO.getAppSecret(appId); |
| | | Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId); |
| | | |
| | | // 2. 校验签名【重要!】 |
| | | String clientSignature = request.getHeader(signature.sign()); // 客户端签名 |
| | | String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串 |
| | | String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名 |
| | | if (ObjUtil.notEqual(clientSignature, serverSignature)) { |
| | | return false; |
| | | } |
| | | |
| | | // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ) |
| | | String nonce = request.getHeader(signature.nonce()); |
| | | signatureRedisDAO.setNonce(nonce, signature.timeout() * 2, signature.timeUnit()); |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * 校验请求头加签参数 |
| | | * |
| | | * 1. appId 是否为空 |
| | | * 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟 |
| | | * 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了 |
| | | * 4. sign 是否为空 |
| | | * |
| | | * @param signature signature |
| | | * @param request request |
| | | * @return 是否校验 Header 通过 |
| | | */ |
| | | private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) { |
| | | // 1. 非空校验 |
| | | String appId = request.getHeader(signature.appId()); |
| | | if (StrUtil.isBlank(appId)) { |
| | | return false; |
| | | } |
| | | String timestamp = request.getHeader(signature.timestamp()); |
| | | if (StrUtil.isBlank(timestamp)) { |
| | | return false; |
| | | } |
| | | String nonce = request.getHeader(signature.nonce()); |
| | | if (StrUtil.length(nonce) < 10) { |
| | | return false; |
| | | } |
| | | String sign = request.getHeader(signature.sign()); |
| | | if (StrUtil.isBlank(sign)) { |
| | | return false; |
| | | } |
| | | |
| | | // 2. 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值) |
| | | long expireTime = signature.timeUnit().toMillis(signature.timeout()); |
| | | long requestTimestamp = Long.parseLong(timestamp); |
| | | long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp); |
| | | if (timestampDisparity > expireTime) { |
| | | return false; |
| | | } |
| | | |
| | | // 3. 检查 nonce 是否存在,有且仅能使用一次 |
| | | return signatureRedisDAO.getNonce(nonce) == null; |
| | | } |
| | | |
| | | /** |
| | | * 构建签名字符串 |
| | | * |
| | | * 格式为 = 请求参数 + 请求体 + 请求头 + 密钥 |
| | | * |
| | | * @param signature signature |
| | | * @param request request |
| | | * @param appSecret appSecret |
| | | * @return 签名字符串 |
| | | */ |
| | | private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) { |
| | | SortedMap<String, String> parameterMap = getRequestParameterMap(request); // 请求头 |
| | | SortedMap<String, String> headerMap = getRequestHeaderMap(signature, request); // 请求参数 |
| | | String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), ""); // 请求体 |
| | | return MapUtil.join(parameterMap, "&", "=") |
| | | + requestBody |
| | | + MapUtil.join(headerMap, "&", "=") |
| | | + appSecret; |
| | | } |
| | | |
| | | /** |
| | | * 获取请求头加签参数 Map |
| | | * |
| | | * @param request 请求 |
| | | * @param signature 签名注解 |
| | | * @return signature params |
| | | */ |
| | | private static SortedMap<String, String> getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) { |
| | | SortedMap<String, String> sortedMap = new TreeMap<>(); |
| | | sortedMap.put(signature.appId(), request.getHeader(signature.appId())); |
| | | sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp())); |
| | | sortedMap.put(signature.nonce(), request.getHeader(signature.nonce())); |
| | | return sortedMap; |
| | | } |
| | | |
| | | /** |
| | | * 获取请求参数 Map |
| | | * |
| | | * @param request 请求 |
| | | * @return queryParams |
| | | */ |
| | | private static SortedMap<String, String> getRequestParameterMap(HttpServletRequest request) { |
| | | SortedMap<String, String> sortedMap = new TreeMap<>(); |
| | | for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) { |
| | | sortedMap.put(entry.getKey(), entry.getValue()[0]); |
| | | } |
| | | return sortedMap; |
| | | } |
| | | |
| | | } |
| | | |
对比新文件 |
| | |
| | | package com.iailab.framework.signature.core.redis; |
| | | |
| | | import lombok.AllArgsConstructor; |
| | | import org.springframework.data.redis.core.StringRedisTemplate; |
| | | |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | | * HTTP API 签名 Redis DAO |
| | | * |
| | | * @author Zhougang |
| | | */ |
| | | @AllArgsConstructor |
| | | public class ApiSignatureRedisDAO { |
| | | |
| | | private final StringRedisTemplate stringRedisTemplate; |
| | | |
| | | /** |
| | | * 验签随机数 |
| | | * |
| | | * KEY 格式:signature_nonce:%s // 参数为 随机数 |
| | | * VALUE 格式:String |
| | | * 过期时间:不固定 |
| | | */ |
| | | private static final String SIGNATURE_NONCE = "api_signature_nonce:%s"; |
| | | |
| | | /** |
| | | * 签名密钥 |
| | | * |
| | | * HASH 结构 |
| | | * KEY 格式:%s // 参数为 appid |
| | | * VALUE 格式:String |
| | | * 过期时间:永不过期(预加载到 Redis) |
| | | */ |
| | | private static final String SIGNATURE_APPID = "api_signature_app"; |
| | | |
| | | // ========== 验签随机数 ========== |
| | | |
| | | public String getNonce(String nonce) { |
| | | return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce)); |
| | | } |
| | | |
| | | public void setNonce(String nonce, int time, TimeUnit timeUnit) { |
| | | stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), "", time, timeUnit); |
| | | } |
| | | |
| | | private static String formatNonceKey(String key) { |
| | | return String.format(SIGNATURE_NONCE, key); |
| | | } |
| | | |
| | | // ========== 签名密钥 ========== |
| | | |
| | | public String getAppSecret(String appId) { |
| | | return (String) stringRedisTemplate.opsForHash().get(SIGNATURE_APPID, appId); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * HTTP API 签名,校验安全性 |
| | | * |
| | | * @see <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3>微信支付 —— 安全规范</a> |
| | | */ |
| | | package com.iailab.framework.signature; |
对比新文件 |
| | |
| | | com.iailab.framework.idempotent.config.IailabIdempotentConfiguration |
| | | com.iailab.framework.lock4j.config.IailabLock4jConfiguration |
| | | com.iailab.framework.ratelimiter.config.IailabRateLimiterConfiguration |
对比新文件 |
| | |
| | | package com.iailab.framework.signature.core; |
| | | |
| | | import cn.hutool.core.map.MapUtil; |
| | | import cn.hutool.core.util.IdUtil; |
| | | import cn.hutool.crypto.digest.DigestUtil; |
| | | import com.iailab.framework.signature.core.annotation.ApiSignature; |
| | | import com.iailab.framework.signature.core.aop.ApiSignatureAspect; |
| | | import com.iailab.framework.signature.core.redis.ApiSignatureRedisDAO; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.junit.jupiter.api.extension.ExtendWith; |
| | | import org.mockito.InjectMocks; |
| | | import org.mockito.Mock; |
| | | import org.mockito.junit.jupiter.MockitoExtension; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.io.BufferedReader; |
| | | import java.io.IOException; |
| | | import java.io.StringReader; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | /** |
| | | * {@link ApiSignatureTest} 的单元测试 |
| | | */ |
| | | @ExtendWith(MockitoExtension.class) |
| | | public class ApiSignatureTest { |
| | | |
| | | @InjectMocks |
| | | private ApiSignatureAspect apiSignatureAspect; |
| | | |
| | | @Mock |
| | | private ApiSignatureRedisDAO signatureRedisDAO; |
| | | |
| | | @Test |
| | | public void testSignatureGet() throws IOException { |
| | | // 搞一个签名 |
| | | Long timestamp = System.currentTimeMillis(); |
| | | String nonce = IdUtil.randomUUID(); |
| | | String appId = "xxxxxx"; |
| | | String appSecret = "yyyyyy"; |
| | | String signString = "k1=v1&v1=k1testappId=xxxxxx&nonce=" + nonce + "×tamp=" + timestamp + "yyyyyy"; |
| | | String sign = DigestUtil.sha256Hex(signString); |
| | | |
| | | // 准备参数 |
| | | ApiSignature apiSignature = mock(ApiSignature.class); |
| | | when(apiSignature.appId()).thenReturn("appId"); |
| | | when(apiSignature.timestamp()).thenReturn("timestamp"); |
| | | when(apiSignature.nonce()).thenReturn("nonce"); |
| | | when(apiSignature.sign()).thenReturn("sign"); |
| | | when(apiSignature.timeout()).thenReturn(60); |
| | | when(apiSignature.timeUnit()).thenReturn(TimeUnit.SECONDS); |
| | | HttpServletRequest request = mock(HttpServletRequest.class); |
| | | when(request.getHeader(eq("appId"))).thenReturn(appId); |
| | | when(request.getHeader(eq("timestamp"))).thenReturn(String.valueOf(timestamp)); |
| | | when(request.getHeader(eq("nonce"))).thenReturn(nonce); |
| | | when(request.getHeader(eq("sign"))).thenReturn(sign); |
| | | when(request.getParameterMap()).thenReturn(MapUtil.<String, String[]>builder() |
| | | .put("v1", new String[]{"k1"}).put("k1", new String[]{"v1"}).build()); |
| | | when(request.getContentType()).thenReturn("application/json"); |
| | | when(request.getReader()).thenReturn(new BufferedReader(new StringReader("test"))); |
| | | // mock 方法 |
| | | when(signatureRedisDAO.getAppSecret(eq(appId))).thenReturn(appSecret); |
| | | |
| | | // 调用 |
| | | boolean result = apiSignatureAspect.verifySignature(apiSignature, request); |
| | | // 断言结果 |
| | | assertTrue(result); |
| | | // 断言调用 |
| | | verify(signatureRedisDAO).setNonce(eq(nonce), eq(120), eq(TimeUnit.SECONDS)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-redis</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>Redis 封装拓展</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- DB 相关 --> |
| | | <dependency> |
| | | <groupId>org.redisson</groupId> |
| | | <artifactId>redisson-spring-boot-starter</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-cache</artifactId> <!-- 实现对 Caches 的自动化配置 --> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.fasterxml.jackson.datatype</groupId> |
| | | <artifactId>jackson-datatype-jsr310</artifactId> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.redis.config; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.redis.core.TimeoutRedisCacheManager; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.cache.CacheProperties; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.cache.annotation.EnableCaching; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Primary; |
| | | import org.springframework.data.redis.cache.BatchStrategies; |
| | | import org.springframework.data.redis.cache.RedisCacheConfiguration; |
| | | import org.springframework.data.redis.cache.RedisCacheManager; |
| | | import org.springframework.data.redis.cache.RedisCacheWriter; |
| | | import org.springframework.data.redis.connection.RedisConnectionFactory; |
| | | import org.springframework.data.redis.core.RedisTemplate; |
| | | import org.springframework.data.redis.serializer.RedisSerializationContext; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.util.Objects; |
| | | |
| | | import static com.iailab.framework.redis.config.IailabRedisAutoConfiguration.buildRedisSerializer; |
| | | |
| | | /** |
| | | * Cache 配置类,基于 Redis 实现 |
| | | */ |
| | | @AutoConfiguration |
| | | @EnableConfigurationProperties({CacheProperties.class, IailabCacheProperties.class}) |
| | | @EnableCaching |
| | | public class IailabCacheAutoConfiguration { |
| | | |
| | | /** |
| | | * RedisCacheConfiguration Bean |
| | | * <p> |
| | | * 参考 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 的 createConfiguration 方法 |
| | | */ |
| | | @Bean |
| | | @Primary |
| | | public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { |
| | | RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); |
| | | // 设置使用 : 单冒号,而不是双 :: 冒号,避免 Redis Desktop Manager 多余空格 |
| | | // 详细可见 https://blog.csdn.net/chuixue24/article/details/103928965 博客 |
| | | // 再次修复单冒号,而不是双 :: 冒号问题,Issues 详情:https://gitee.com/zhijiantianya/iailab-cloud/issues/I86VY2 |
| | | config = config.computePrefixWith(cacheName -> { |
| | | String keyPrefix = cacheProperties.getRedis().getKeyPrefix(); |
| | | if (StringUtils.hasText(keyPrefix)) { |
| | | keyPrefix = keyPrefix.lastIndexOf(StrUtil.COLON) == -1 ? keyPrefix + StrUtil.COLON : keyPrefix; |
| | | return keyPrefix + cacheName + StrUtil.COLON; |
| | | } |
| | | return cacheName + StrUtil.COLON; |
| | | }); |
| | | // 设置使用 JSON 序列化方式 |
| | | config = config.serializeValuesWith( |
| | | RedisSerializationContext.SerializationPair.fromSerializer(buildRedisSerializer())); |
| | | |
| | | // 设置 CacheProperties.Redis 的属性 |
| | | CacheProperties.Redis redisProperties = cacheProperties.getRedis(); |
| | | if (redisProperties.getTimeToLive() != null) { |
| | | config = config.entryTtl(redisProperties.getTimeToLive()); |
| | | } |
| | | if (!redisProperties.isCacheNullValues()) { |
| | | config = config.disableCachingNullValues(); |
| | | } |
| | | if (!redisProperties.isUseKeyPrefix()) { |
| | | config = config.disableKeyPrefix(); |
| | | } |
| | | return config; |
| | | } |
| | | |
| | | @Bean |
| | | public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate, |
| | | RedisCacheConfiguration redisCacheConfiguration, |
| | | IailabCacheProperties iailabCacheProperties) { |
| | | // 创建 RedisCacheWriter 对象 |
| | | RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); |
| | | RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, |
| | | BatchStrategies.scan(iailabCacheProperties.getRedisScanBatchSize())); |
| | | // 创建 TenantRedisCacheManager 对象 |
| | | return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.redis.config; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | /** |
| | | * Cache 配置项 |
| | | * |
| | | * @author Wanwan |
| | | */ |
| | | @ConfigurationProperties("iailab.cache") |
| | | @Data |
| | | @Validated |
| | | public class IailabCacheProperties { |
| | | |
| | | /** |
| | | * {@link #redisScanBatchSize} 默认值 |
| | | */ |
| | | private static final Integer REDIS_SCAN_BATCH_SIZE_DEFAULT = 30; |
| | | |
| | | /** |
| | | * redis scan 一次返回数量 |
| | | */ |
| | | private Integer redisScanBatchSize = REDIS_SCAN_BATCH_SIZE_DEFAULT; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.redis.config; |
| | | |
| | | import cn.hutool.core.util.ReflectUtil; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
| | | import org.redisson.spring.starter.RedissonAutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.data.redis.connection.RedisConnectionFactory; |
| | | import org.springframework.data.redis.core.RedisTemplate; |
| | | import org.springframework.data.redis.serializer.RedisSerializer; |
| | | |
| | | /** |
| | | * Redis 配置类 |
| | | */ |
| | | @AutoConfiguration(before = RedissonAutoConfiguration.class) // 目的:使用自己定义的 RedisTemplate Bean |
| | | public class IailabRedisAutoConfiguration { |
| | | |
| | | /** |
| | | * 创建 RedisTemplate Bean,使用 JSON 序列化方式 |
| | | */ |
| | | @Bean |
| | | public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { |
| | | // 创建 RedisTemplate 对象 |
| | | RedisTemplate<String, Object> template = new RedisTemplate<>(); |
| | | // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。 |
| | | template.setConnectionFactory(factory); |
| | | // 使用 String 序列化方式,序列化 KEY 。 |
| | | template.setKeySerializer(RedisSerializer.string()); |
| | | template.setHashKeySerializer(RedisSerializer.string()); |
| | | // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 |
| | | template.setValueSerializer(buildRedisSerializer()); |
| | | template.setHashValueSerializer(buildRedisSerializer()); |
| | | return template; |
| | | } |
| | | |
| | | public static RedisSerializer<?> buildRedisSerializer() { |
| | | RedisSerializer<Object> json = RedisSerializer.json(); |
| | | // 解决 LocalDateTime 的序列化 |
| | | ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper"); |
| | | objectMapper.registerModules(new JavaTimeModule()); |
| | | return json; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.redis.core; |
| | | |
| | | import cn.hutool.core.util.NumberUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import org.springframework.cache.annotation.Cacheable; |
| | | import org.springframework.data.redis.cache.RedisCache; |
| | | import org.springframework.data.redis.cache.RedisCacheConfiguration; |
| | | import org.springframework.data.redis.cache.RedisCacheManager; |
| | | import org.springframework.data.redis.cache.RedisCacheWriter; |
| | | |
| | | import java.time.Duration; |
| | | |
| | | /** |
| | | * 支持自定义过期时间的 {@link RedisCacheManager} 实现类 |
| | | * |
| | | * 在 {@link Cacheable#cacheNames()} 格式为 "key#ttl" 时,# 后面的 ttl 为过期时间。 |
| | | * 单位为最后一个字母(支持的单位有:d 天,h 小时,m 分钟,s 秒),默认单位为 s 秒 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TimeoutRedisCacheManager extends RedisCacheManager { |
| | | |
| | | private static final String SPLIT = "#"; |
| | | |
| | | public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) { |
| | | super(cacheWriter, defaultCacheConfiguration); |
| | | } |
| | | |
| | | @Override |
| | | protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) { |
| | | if (StrUtil.isEmpty(name)) { |
| | | return super.createRedisCache(name, cacheConfig); |
| | | } |
| | | // 如果使用 # 分隔,大小不为 2,则说明不使用自定义过期时间 |
| | | String[] names = StrUtil.splitToArray(name, SPLIT); |
| | | if (names.length != 2) { |
| | | return super.createRedisCache(name, cacheConfig); |
| | | } |
| | | |
| | | // 核心:通过修改 cacheConfig 的过期时间,实现自定义过期时间 |
| | | if (cacheConfig != null) { |
| | | // 移除 # 后面的 : 以及后面的内容,避免影响解析 |
| | | String ttlStr = StrUtil.subBefore(names[1], StrUtil.COLON, false); // 获得 ttlStr 时间部分 |
| | | names[1] = StrUtil.subAfter(names[1], ttlStr, false); // 移除掉 ttlStr 时间部分 |
| | | // 解析时间 |
| | | Duration duration = parseDuration(ttlStr); |
| | | cacheConfig = cacheConfig.entryTtl(duration); |
| | | } |
| | | |
| | | // 创建 RedisCache 对象,需要忽略掉 ttlStr |
| | | return super.createRedisCache(names[0] + names[1], cacheConfig); |
| | | } |
| | | |
| | | /** |
| | | * 解析过期时间 Duration |
| | | * |
| | | * @param ttlStr 过期时间字符串 |
| | | * @return 过期时间 Duration |
| | | */ |
| | | private Duration parseDuration(String ttlStr) { |
| | | String timeUnit = StrUtil.subSuf(ttlStr, -1); |
| | | switch (timeUnit) { |
| | | case "d": |
| | | return Duration.ofDays(removeDurationSuffix(ttlStr)); |
| | | case "h": |
| | | return Duration.ofHours(removeDurationSuffix(ttlStr)); |
| | | case "m": |
| | | return Duration.ofMinutes(removeDurationSuffix(ttlStr)); |
| | | case "s": |
| | | return Duration.ofSeconds(removeDurationSuffix(ttlStr)); |
| | | default: |
| | | return Duration.ofSeconds(Long.parseLong(ttlStr)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 移除多余的后缀,返回具体的时间 |
| | | * |
| | | * @param ttlStr 过期时间字符串 |
| | | * @return 时间 |
| | | */ |
| | | private Long removeDurationSuffix(String ttlStr) { |
| | | return NumberUtil.parseLong(StrUtil.sub(ttlStr, 0, ttlStr.length() - 1)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 采用 Spring Data Redis 操作 Redis,底层使用 Redisson 作为客户端 |
| | | */ |
| | | package com.iailab.framework.redis; |
对比新文件 |
| | |
| | | com.iailab.framework.redis.config.IailabRedisAutoConfiguration |
| | | com.iailab.framework.redis.config.IailabCacheAutoConfiguration |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <groupId>com.iailab</groupId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-rpc</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description> |
| | | OpenFeign:提供 RESTful API 的调用 |
| | | </description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- RPC 远程调用相关 --> |
| | | <dependency> |
| | | <groupId>org.springframework.cloud</groupId> |
| | | <artifactId>spring-cloud-starter-loadbalancer</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.cloud</groupId> |
| | | <artifactId>spring-cloud-starter-openfeign</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>io.github.openfeign</groupId> |
| | | <artifactId>feign-okhttp</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- 工具相关 --> |
| | | <dependency> |
| | | <groupId>jakarta.validation</groupId> |
| | | <artifactId>jakarta.validation-api</artifactId> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | /** |
| | | * 占坑 TODO |
| | | */ |
| | | package com.iailab.framework.rpc.config; |
对比新文件 |
| | |
| | | /** |
| | | * 占坑 TODO |
| | | */ |
| | | package com.iailab.framework.rpc.core; |
对比新文件 |
| | |
| | | /** |
| | | * OpenFeign:提供 RESTful API 的调用 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | package com.iailab.framework.rpc; |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-security</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description> |
| | | 1. security:用户的认证、权限的校验,实现「谁」可以做「什么事」 |
| | | 2. operatelog:操作日志,实现「谁」在「什么时间」对「什么」做了「什么事」 |
| | | </description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Spring 核心 --> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-aop</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Web 相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-web</artifactId> |
| | | </dependency> |
| | | <!-- spring boot 配置所需依赖 --> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-configuration-processor</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-security</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- RPC 远程调用相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-rpc</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <!-- 业务组件 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-module-system-api</artifactId> <!-- 需要使用它,进行 Token 的校验 --> |
| | | <version>${revision}</version> |
| | | </dependency> |
| | | |
| | | <!-- 工具类相关 --> |
| | | <dependency> |
| | | <groupId>com.google.guava</groupId> |
| | | <artifactId>guava</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <!-- Spring Boot 通用操作日志组件,基于注解实现 --> |
| | | <!-- 此组件解决的问题是:「谁」在「什么时间」对「什么」做了「什么事」 --> |
| | | <groupId>io.github.mouzt</groupId> |
| | | <artifactId>bizlog-sdk</artifactId> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.operatelog.config; |
| | | |
| | | import com.iailab.framework.operatelog.core.service.LogRecordServiceImpl; |
| | | import com.mzt.logapi.service.ILogRecordService; |
| | | import com.mzt.logapi.starter.annotation.EnableLogRecord; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Primary; |
| | | |
| | | /** |
| | | * 操作日志配置类 |
| | | * |
| | | * @author HUIHUI |
| | | */ |
| | | @EnableLogRecord(tenant = "") // 貌似用不上 tenant 这玩意给个空好啦 |
| | | @AutoConfiguration |
| | | @Slf4j |
| | | public class IailabOperateLogConfiguration { |
| | | |
| | | @Bean |
| | | @Primary |
| | | public ILogRecordService iLogRecordServiceImpl() { |
| | | return new LogRecordServiceImpl(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.operatelog.config; |
| | | |
| | | import com.iailab.module.system.api.logger.OperateLogApi; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.cloud.openfeign.EnableFeignClients; |
| | | |
| | | /** |
| | | * OperateLog 使用到 Feign 的配置项 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @EnableFeignClients(clients = {OperateLogApi.class}) // 主要是引入相关的 API 服务 |
| | | public class IailabOperateLogRpcAutoConfiguration { |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 占位,无特殊作用 |
| | | */ |
| | | package com.iailab.framework.operatelog.core; |
对比新文件 |
| | |
| | | package com.iailab.framework.operatelog.core.service; |
| | | |
| | | import com.iailab.framework.common.util.monitor.TracerUtils; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import com.iailab.framework.security.core.LoginUser; |
| | | import com.iailab.framework.security.core.util.SecurityFrameworkUtils; |
| | | import com.iailab.module.system.api.logger.OperateLogApi; |
| | | import com.iailab.module.system.api.logger.dto.OperateLogCreateReqDTO; |
| | | import com.mzt.logapi.beans.LogRecord; |
| | | import com.mzt.logapi.service.ILogRecordService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 操作日志 ILogRecordService 实现类 |
| | | * |
| | | * 基于 {@link OperateLogApi} 实现,记录操作日志 |
| | | * |
| | | * @author HUIHUI |
| | | */ |
| | | @Slf4j |
| | | public class LogRecordServiceImpl implements ILogRecordService { |
| | | |
| | | @Resource |
| | | private OperateLogApi operateLogApi; |
| | | |
| | | @Override |
| | | public void record(LogRecord logRecord) { |
| | | // 1. 补全通用字段 |
| | | OperateLogCreateReqDTO reqDTO = new OperateLogCreateReqDTO(); |
| | | reqDTO.setTraceId(TracerUtils.getTraceId()); |
| | | // 补充用户信息 |
| | | fillUserFields(reqDTO); |
| | | // 补全模块信息 |
| | | fillModuleFields(reqDTO, logRecord); |
| | | // 补全请求信息 |
| | | fillRequestFields(reqDTO); |
| | | |
| | | // 2. 异步记录日志 |
| | | operateLogApi.createOperateLog(reqDTO); |
| | | } |
| | | |
| | | private static void fillUserFields(OperateLogCreateReqDTO reqDTO) { |
| | | // 使用 SecurityFrameworkUtils。因为要考虑,rpc、mq、job,它其实不是 web; |
| | | LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); |
| | | if (loginUser == null) { |
| | | return; |
| | | } |
| | | reqDTO.setUserId(loginUser.getId()); |
| | | reqDTO.setUserType(loginUser.getUserType()); |
| | | } |
| | | |
| | | public static void fillModuleFields(OperateLogCreateReqDTO reqDTO, LogRecord logRecord) { |
| | | reqDTO.setType(logRecord.getType()); // 大模块类型,例如:CRM 客户 |
| | | reqDTO.setSubType(logRecord.getSubType());// 操作名称,例如:转移客户 |
| | | reqDTO.setBizId(Long.parseLong(logRecord.getBizNo())); // 业务编号,例如:客户编号 |
| | | reqDTO.setAction(logRecord.getAction());// 操作内容,例如:修改编号为 1 的用户信息,将性别从男改成女,将姓名从平台改成源码。 |
| | | reqDTO.setExtra(logRecord.getExtra()); // 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ),例如说,记录订单编号,{ orderId: "1"} |
| | | } |
| | | |
| | | private static void fillRequestFields(OperateLogCreateReqDTO reqDTO) { |
| | | // 获得 Request 对象 |
| | | HttpServletRequest request = ServletUtils.getRequest(); |
| | | if (request == null) { |
| | | return; |
| | | } |
| | | // 补全请求信息 |
| | | reqDTO.setRequestMethod(request.getMethod()); |
| | | reqDTO.setRequestUrl(request.getRequestURI()); |
| | | reqDTO.setUserIp(ServletUtils.getClientIP(request)); |
| | | reqDTO.setUserAgent(ServletUtils.getUserAgent(request)); |
| | | } |
| | | |
| | | @Override |
| | | public List<LogRecord> queryLog(String bizNo, String type) { |
| | | throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询"); |
| | | } |
| | | |
| | | @Override |
| | | public List<LogRecord> queryLogByBizNo(String bizNo, String type, String subType) { |
| | | throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询"); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 基于 mzt-log 框架 |
| | | * 实现操作日志功能 |
| | | * |
| | | * @author HUIHUI |
| | | */ |
| | | package com.iailab.framework.operatelog; |
对比新文件 |
| | |
| | | package com.iailab.framework.security.config; |
| | | |
| | | import com.iailab.framework.web.config.WebProperties; |
| | | import org.springframework.core.Ordered; |
| | | import org.springframework.security.config.Customizer; |
| | | import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
| | | import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; |
| | | import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; |
| | | |
| | | import javax.annotation.Resource; |
| | | |
| | | /** |
| | | * 自定义的 URL 的安全配置 |
| | | * 目的:每个 Maven Module 可以自定义规则! |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public abstract class AuthorizeRequestsCustomizer |
| | | implements Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry>, Ordered { |
| | | |
| | | @Resource |
| | | private WebProperties webProperties; |
| | | |
| | | protected String buildAdminApi(String url) { |
| | | return webProperties.getAdminApi().getPrefix() + url; |
| | | } |
| | | |
| | | protected String buildAppApi(String url) { |
| | | return webProperties.getAppApi().getPrefix() + url; |
| | | } |
| | | |
| | | @Override |
| | | public int getOrder() { |
| | | return 0; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.config; |
| | | |
| | | import cn.hutool.extra.spring.SpringUtil; |
| | | import com.iailab.framework.security.core.aop.PreAuthenticatedAspect; |
| | | import com.iailab.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy; |
| | | import com.iailab.framework.security.core.filter.TokenAuthenticationFilter; |
| | | import com.iailab.framework.security.core.handler.AccessDeniedHandlerImpl; |
| | | import com.iailab.framework.security.core.handler.AuthenticationEntryPointImpl; |
| | | import com.iailab.framework.security.core.service.SecurityFrameworkService; |
| | | import com.iailab.framework.security.core.service.SecurityFrameworkServiceImpl; |
| | | import com.iailab.framework.web.core.handler.GlobalExceptionHandler; |
| | | import com.iailab.module.system.api.oauth2.OAuth2TokenApi; |
| | | import com.iailab.module.system.api.permission.PermissionApi; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.beans.factory.config.MethodInvokingFactoryBean; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.AutoConfigureOrder; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.security.core.context.SecurityContextHolder; |
| | | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
| | | import org.springframework.security.crypto.password.PasswordEncoder; |
| | | import org.springframework.security.web.AuthenticationEntryPoint; |
| | | import org.springframework.security.web.access.AccessDeniedHandler; |
| | | |
| | | import javax.annotation.Resource; |
| | | |
| | | /** |
| | | * Spring Security 自动配置类,主要用于相关组件的配置 |
| | | * |
| | | * 注意,不能和 {@link IailabWebSecurityConfigurerAdapter} 用一个,原因是会导致初始化报错。 |
| | | * 参见 https://stackoverflow.com/questions/53847050/spring-boot-delegatebuilder-cannot-be-null-on-autowiring-authenticationmanager 文档。 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效 |
| | | @EnableConfigurationProperties(SecurityProperties.class) |
| | | public class IailabSecurityAutoConfiguration { |
| | | |
| | | @Resource |
| | | private SecurityProperties securityProperties; |
| | | |
| | | /** |
| | | * 处理用户未登录拦截的切面的 Bean |
| | | */ |
| | | @Bean |
| | | public PreAuthenticatedAspect preAuthenticatedAspect() { |
| | | return new PreAuthenticatedAspect(); |
| | | } |
| | | |
| | | /** |
| | | * 认证失败处理类 Bean |
| | | */ |
| | | @Bean |
| | | public AuthenticationEntryPoint authenticationEntryPoint() { |
| | | return new AuthenticationEntryPointImpl(); |
| | | } |
| | | |
| | | /** |
| | | * 权限不够处理器 Bean |
| | | */ |
| | | @Bean |
| | | public AccessDeniedHandler accessDeniedHandler() { |
| | | return new AccessDeniedHandlerImpl(); |
| | | } |
| | | |
| | | /** |
| | | * Spring Security 加密器 |
| | | * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器 |
| | | * |
| | | * @see <a href="http://stackabuse.com/password-encoding-with-spring-security/">Password Encoding with Spring Security</a> |
| | | */ |
| | | @Bean |
| | | public PasswordEncoder passwordEncoder() { |
| | | return new BCryptPasswordEncoder(securityProperties.getPasswordEncoderLength()); |
| | | } |
| | | |
| | | /** |
| | | * Token 认证过滤器 Bean |
| | | */ |
| | | @Bean |
| | | public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler, |
| | | OAuth2TokenApi oauth2TokenApi) { |
| | | try { |
| | | OAuth2TokenApi oAuth2TokenApi = SpringUtil.getBean("aAuth2TokenApiImpl", OAuth2TokenApi.class); |
| | | if (oAuth2TokenApi != null) { |
| | | oauth2TokenApi = oAuth2TokenApi; |
| | | } |
| | | } catch (Exception ignored) {} |
| | | return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi); |
| | | } |
| | | |
| | | @Bean("ss") // 使用 Spring Security 的缩写,方便使用 |
| | | public SecurityFrameworkService securityFrameworkService(PermissionApi permissionApi) { |
| | | try { |
| | | PermissionApi permissionApiImpl = SpringUtil.getBean("permissionApiImpl", PermissionApi.class); |
| | | if (permissionApiImpl != null) { |
| | | permissionApi = permissionApiImpl; |
| | | } |
| | | } catch (Exception ignored) {} |
| | | return new SecurityFrameworkServiceImpl(permissionApi); |
| | | } |
| | | |
| | | /** |
| | | * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法, |
| | | * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略 |
| | | */ |
| | | @Bean |
| | | public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() { |
| | | MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean(); |
| | | methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class); |
| | | methodInvokingFactoryBean.setTargetMethod("setStrategyName"); |
| | | methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName()); |
| | | return methodInvokingFactoryBean; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.config; |
| | | |
| | | import com.iailab.framework.security.core.rpc.LoginUserRequestInterceptor; |
| | | import com.iailab.module.system.api.oauth2.OAuth2TokenApi; |
| | | import com.iailab.module.system.api.permission.PermissionApi; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.cloud.openfeign.EnableFeignClients; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | /** |
| | | * Security 使用到 Feign 的配置项 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @EnableFeignClients(clients = {OAuth2TokenApi.class, // 主要是引入相关的 API 服务 |
| | | PermissionApi.class}) |
| | | public class IailabSecurityRpcAutoConfiguration { |
| | | |
| | | @Bean |
| | | public LoginUserRequestInterceptor loginUserRequestInterceptor() { |
| | | return new LoginUserRequestInterceptor(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.config; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.iailab.framework.security.core.filter.TokenAuthenticationFilter; |
| | | import com.iailab.framework.web.config.WebProperties; |
| | | import com.google.common.collect.HashMultimap; |
| | | import com.google.common.collect.Multimap; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.AutoConfigureOrder; |
| | | import org.springframework.context.ApplicationContext; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.http.HttpMethod; |
| | | import org.springframework.security.authentication.AuthenticationManager; |
| | | import org.springframework.security.config.Customizer; |
| | | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; |
| | | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; |
| | | import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
| | | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; |
| | | import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; |
| | | import org.springframework.security.config.http.SessionCreationPolicy; |
| | | import org.springframework.security.web.AuthenticationEntryPoint; |
| | | import org.springframework.security.web.SecurityFilterChain; |
| | | import org.springframework.security.web.access.AccessDeniedHandler; |
| | | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
| | | import org.springframework.web.bind.annotation.RequestMethod; |
| | | import org.springframework.web.method.HandlerMethod; |
| | | import org.springframework.web.servlet.mvc.method.RequestMappingInfo; |
| | | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; |
| | | import org.springframework.web.util.pattern.PathPattern; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.annotation.security.PermitAll; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import static com.iailab.framework.common.util.collection.CollectionUtils.convertList; |
| | | |
| | | /** |
| | | * 自定义的 Spring Security 配置适配器实现 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效 |
| | | @EnableMethodSecurity(securedEnabled = true) |
| | | public class IailabWebSecurityConfigurerAdapter { |
| | | |
| | | @Resource |
| | | private WebProperties webProperties; |
| | | @Resource |
| | | private SecurityProperties securityProperties; |
| | | |
| | | /** |
| | | * 认证失败处理类 Bean |
| | | */ |
| | | @Resource |
| | | private AuthenticationEntryPoint authenticationEntryPoint; |
| | | /** |
| | | * 权限不够处理器 Bean |
| | | */ |
| | | @Resource |
| | | private AccessDeniedHandler accessDeniedHandler; |
| | | /** |
| | | * Token 认证过滤器 Bean |
| | | */ |
| | | @Resource |
| | | private TokenAuthenticationFilter authenticationTokenFilter; |
| | | |
| | | /** |
| | | * 自定义的权限映射 Bean 们 |
| | | * |
| | | * @see #filterChain(HttpSecurity) |
| | | */ |
| | | @Resource |
| | | private List<AuthorizeRequestsCustomizer> authorizeRequestsCustomizers; |
| | | |
| | | @Resource |
| | | private ApplicationContext applicationContext; |
| | | |
| | | /** |
| | | * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入 |
| | | * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题 |
| | | */ |
| | | @Bean |
| | | public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception { |
| | | return authenticationConfiguration.getAuthenticationManager(); |
| | | } |
| | | |
| | | /** |
| | | * 配置 URL 的安全配置 |
| | | * |
| | | * anyRequest | 匹配所有请求路径 |
| | | * access | SpringEl表达式结果为true时可以访问 |
| | | * anonymous | 匿名可以访问 |
| | | * denyAll | 用户不能访问 |
| | | * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) |
| | | * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 |
| | | * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 |
| | | * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 |
| | | * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 |
| | | * hasRole | 如果有参数,参数表示角色,则其角色可以访问 |
| | | * permitAll | 用户可以任意访问 |
| | | * rememberMe | 允许通过remember-me登录的用户访问 |
| | | * authenticated | 用户登录后可访问 |
| | | */ |
| | | @Bean |
| | | protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { |
| | | // 登出 |
| | | httpSecurity |
| | | // 开启跨域 |
| | | .cors(Customizer.withDefaults()) |
| | | // CSRF 禁用,因为不使用 Session |
| | | .csrf(AbstractHttpConfigurer::disable) |
| | | // 基于 token 机制,所以不需要 Session |
| | | .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) |
| | | .headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) |
| | | // 一堆自定义的 Spring Security 处理器 |
| | | .exceptionHandling(c -> c.authenticationEntryPoint(authenticationEntryPoint) |
| | | .accessDeniedHandler(accessDeniedHandler)); |
| | | // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 |
| | | |
| | | // 获得 @PermitAll 带来的 URL 列表,免登录 |
| | | Multimap<HttpMethod, String> permitAllUrls = getPermitAllUrlsFromAnnotations(); |
| | | // 设置每个请求的权限 |
| | | httpSecurity |
| | | // ①:全局共享规则 |
| | | .authorizeHttpRequests(c -> c |
| | | // 1.1 静态资源,可匿名访问 |
| | | .requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll() |
| | | // 1.2 设置 @PermitAll 无需认证 |
| | | .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() |
| | | .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() |
| | | .requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() |
| | | .requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() |
| | | .requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll() |
| | | .requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll() |
| | | // 1.3 基于 yudao.security.permit-all-urls 无需认证 |
| | | .requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() |
| | | ) |
| | | // ②:每个项目的自定义规则 |
| | | .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c))) |
| | | // ③:兜底规则,必须认证 |
| | | .authorizeHttpRequests(c -> c.anyRequest().authenticated()); |
| | | |
| | | // 添加 Token Filter |
| | | httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); |
| | | return httpSecurity.build(); |
| | | } |
| | | |
| | | private String buildAppApi(String url) { |
| | | return webProperties.getAppApi().getPrefix() + url; |
| | | } |
| | | |
| | | private Multimap<HttpMethod, String> getPermitAllUrlsFromAnnotations() { |
| | | Multimap<HttpMethod, String> result = HashMultimap.create(); |
| | | // 获得接口对应的 HandlerMethod 集合 |
| | | RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) |
| | | applicationContext.getBean("requestMappingHandlerMapping"); |
| | | Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); |
| | | // 获得有 @PermitAll 注解的接口 |
| | | for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) { |
| | | HandlerMethod handlerMethod = entry.getValue(); |
| | | if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) { |
| | | continue; |
| | | } |
| | | Set<String> urls = new HashSet<>(); |
| | | if (entry.getKey().getPatternsCondition() != null) { |
| | | urls.addAll(entry.getKey().getPatternsCondition().getPatterns()); |
| | | } |
| | | if (entry.getKey().getPathPatternsCondition() != null) { |
| | | urls.addAll(convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString)); |
| | | } |
| | | if (urls.isEmpty()) { |
| | | continue; |
| | | } |
| | | |
| | | // 特殊:使用 @RequestMapping 注解,并且未写 method 属性,此时认为都需要免登录 |
| | | Set<RequestMethod> methods = entry.getKey().getMethodsCondition().getMethods(); |
| | | if (CollUtil.isEmpty(methods)) { |
| | | result.putAll(HttpMethod.GET, urls); |
| | | result.putAll(HttpMethod.POST, urls); |
| | | result.putAll(HttpMethod.PUT, urls); |
| | | result.putAll(HttpMethod.DELETE, urls); |
| | | result.putAll(HttpMethod.HEAD, urls); |
| | | result.putAll(HttpMethod.PATCH, urls); |
| | | continue; |
| | | } |
| | | // 根据请求方法,添加到 result 结果 |
| | | entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> { |
| | | switch (requestMethod) { |
| | | case GET: |
| | | result.putAll(HttpMethod.GET, urls); |
| | | break; |
| | | case POST: |
| | | result.putAll(HttpMethod.POST, urls); |
| | | break; |
| | | case PUT: |
| | | result.putAll(HttpMethod.PUT, urls); |
| | | break; |
| | | case DELETE: |
| | | result.putAll(HttpMethod.DELETE, urls); |
| | | break; |
| | | case HEAD: |
| | | result.putAll(HttpMethod.HEAD, urls); |
| | | break; |
| | | case PATCH: |
| | | result.putAll(HttpMethod.PATCH, urls); |
| | | break; |
| | | } |
| | | }); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.config; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import javax.validation.constraints.NotEmpty; |
| | | import javax.validation.constraints.NotNull; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | @ConfigurationProperties(prefix = "iailab.security") |
| | | @Validated |
| | | @Data |
| | | public class SecurityProperties { |
| | | |
| | | /** |
| | | * HTTP 请求时,访问令牌的请求 Header |
| | | */ |
| | | @NotEmpty(message = "Token Header 不能为空") |
| | | private String tokenHeader = "Authorization"; |
| | | /** |
| | | * HTTP 请求时,访问令牌的请求参数 |
| | | * |
| | | * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接 |
| | | */ |
| | | @NotEmpty(message = "Token Parameter 不能为空") |
| | | private String tokenParameter = "token"; |
| | | |
| | | /** |
| | | * mock 模式的开关 |
| | | */ |
| | | @NotNull(message = "mock 模式的开关不能为空") |
| | | private Boolean mockEnable = false; |
| | | /** |
| | | * mock 模式的密钥 |
| | | * 一定要配置密钥,保证安全性 |
| | | */ |
| | | @NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。 |
| | | private String mockSecret = "test"; |
| | | |
| | | /** |
| | | * 免登录的 URL 列表 |
| | | */ |
| | | private List<String> permitAllUrls = Collections.emptyList(); |
| | | |
| | | /** |
| | | * PasswordEncoder 加密复杂度,越高开销越大 |
| | | */ |
| | | private Integer passwordEncoderLength = 4; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.core; |
| | | |
| | | import cn.hutool.core.map.MapUtil; |
| | | import com.iailab.framework.common.enums.UserTypeEnum; |
| | | import com.fasterxml.jackson.annotation.JsonIgnore; |
| | | import lombok.Data; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 登录用户信息 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Data |
| | | public class LoginUser { |
| | | |
| | | public static final String INFO_KEY_NICKNAME = "nickname"; |
| | | public static final String INFO_KEY_DEPT_ID = "deptId"; |
| | | |
| | | /** |
| | | * 用户编号 |
| | | */ |
| | | private Long id; |
| | | /** |
| | | * 用户类型 |
| | | * |
| | | * 关联 {@link UserTypeEnum} |
| | | */ |
| | | private Integer userType; |
| | | /** |
| | | * 额外的用户信息 |
| | | */ |
| | | private Map<String, String> info; |
| | | /** |
| | | * 租户编号 |
| | | */ |
| | | private Long tenantId; |
| | | /** |
| | | * 授权范围 |
| | | */ |
| | | private List<String> scopes; |
| | | /** |
| | | * 过期时间 |
| | | */ |
| | | private LocalDateTime expiresTime; |
| | | /** |
| | | * 访问令牌 |
| | | */ |
| | | private String accessToken; |
| | | |
| | | // ========== 上下文 ========== |
| | | /** |
| | | * 上下文字段,不进行持久化 |
| | | * |
| | | * 1. 用于基于 LoginUser 维度的临时缓存 |
| | | */ |
| | | @JsonIgnore |
| | | private Map<String, Object> context; |
| | | |
| | | public void setContext(String key, Object value) { |
| | | if (context == null) { |
| | | context = new HashMap<>(); |
| | | } |
| | | context.put(key, value); |
| | | } |
| | | |
| | | public <T> T getContext(String key, Class<T> type) { |
| | | return MapUtil.get(context, key, type); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.core.annotations; |
| | | |
| | | import java.lang.annotation.*; |
| | | |
| | | /** |
| | | * 声明用户需要登录 |
| | | * |
| | | * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登录 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Target({ElementType.METHOD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Inherited |
| | | @Documented |
| | | public @interface PreAuthenticated { |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.core.aop; |
| | | |
| | | import com.iailab.framework.security.core.annotations.PreAuthenticated; |
| | | import com.iailab.framework.security.core.util.SecurityFrameworkUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.aspectj.lang.ProceedingJoinPoint; |
| | | import org.aspectj.lang.annotation.Around; |
| | | import org.aspectj.lang.annotation.Aspect; |
| | | |
| | | import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; |
| | | import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | |
| | | @Aspect |
| | | @Slf4j |
| | | public class PreAuthenticatedAspect { |
| | | |
| | | @Around("@annotation(preAuthenticated)") |
| | | public Object around(ProceedingJoinPoint joinPoint, PreAuthenticated preAuthenticated) throws Throwable { |
| | | if (SecurityFrameworkUtils.getLoginUser() == null) { |
| | | throw exception(UNAUTHORIZED); |
| | | } |
| | | return joinPoint.proceed(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.core.context; |
| | | |
| | | import com.alibaba.ttl.TransmittableThreadLocal; |
| | | import org.springframework.security.core.context.SecurityContext; |
| | | import org.springframework.security.core.context.SecurityContextHolderStrategy; |
| | | import org.springframework.security.core.context.SecurityContextImpl; |
| | | import org.springframework.util.Assert; |
| | | |
| | | /** |
| | | * 基于 TransmittableThreadLocal 实现的 Security Context 持有者策略 |
| | | * 目的是,避免 @Async 等异步执行时,原生 ThreadLocal 的丢失问题 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { |
| | | |
| | | /** |
| | | * 使用 TransmittableThreadLocal 作为上下文 |
| | | */ |
| | | private static final ThreadLocal<SecurityContext> CONTEXT_HOLDER = new TransmittableThreadLocal<>(); |
| | | |
| | | @Override |
| | | public void clearContext() { |
| | | CONTEXT_HOLDER.remove(); |
| | | } |
| | | |
| | | @Override |
| | | public SecurityContext getContext() { |
| | | SecurityContext ctx = CONTEXT_HOLDER.get(); |
| | | if (ctx == null) { |
| | | ctx = createEmptyContext(); |
| | | CONTEXT_HOLDER.set(ctx); |
| | | } |
| | | return ctx; |
| | | } |
| | | |
| | | @Override |
| | | public void setContext(SecurityContext context) { |
| | | Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); |
| | | CONTEXT_HOLDER.set(context); |
| | | } |
| | | |
| | | @Override |
| | | public SecurityContext createEmptyContext() { |
| | | return new SecurityContextImpl(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.core.filter; |
| | | |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.exception.ServiceException; |
| | | import com.iailab.framework.common.pojo.CommonResult; |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import com.iailab.framework.security.config.SecurityProperties; |
| | | import com.iailab.framework.security.core.LoginUser; |
| | | import com.iailab.framework.security.core.util.SecurityFrameworkUtils; |
| | | import com.iailab.framework.web.core.handler.GlobalExceptionHandler; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import com.iailab.module.system.api.oauth2.OAuth2TokenApi; |
| | | import com.iailab.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.SneakyThrows; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.security.access.AccessDeniedException; |
| | | import org.springframework.web.filter.OncePerRequestFilter; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | import java.net.URLDecoder; |
| | | import java.nio.charset.StandardCharsets; |
| | | |
| | | /** |
| | | * Token 过滤器,验证 token 的有效性 |
| | | * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | @Slf4j |
| | | public class TokenAuthenticationFilter extends OncePerRequestFilter { |
| | | |
| | | private final SecurityProperties securityProperties; |
| | | |
| | | private final GlobalExceptionHandler globalExceptionHandler; |
| | | |
| | | private final OAuth2TokenApi oauth2TokenApi; |
| | | |
| | | @Override |
| | | @SuppressWarnings("NullableProblems") |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
| | | throws ServletException, IOException { |
| | | // 情况一,基于 header[login-user] 获得用户,例如说来自 Gateway 或者其它服务透传 |
| | | LoginUser loginUser = buildLoginUserByHeader(request); |
| | | |
| | | // 情况二,基于 Token 获得用户 |
| | | // 注意,这里主要满足直接使用 Nginx 直接转发到 Spring Cloud 服务的场景。 |
| | | if (loginUser == null) { |
| | | String token = SecurityFrameworkUtils.obtainAuthorization(request, |
| | | securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); |
| | | if (StrUtil.isNotEmpty(token)) { |
| | | Integer userType = WebFrameworkUtils.getLoginUserType(request); |
| | | try { |
| | | // 1.1 基于 token 构建登录用户 |
| | | loginUser = buildLoginUserByToken(token, userType); |
| | | // 1.2 模拟 Login 功能,方便日常开发调试 |
| | | if (loginUser == null) { |
| | | loginUser = mockLoginUser(request, token, userType); |
| | | } |
| | | } catch (Throwable ex) { |
| | | CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex); |
| | | ServletUtils.writeJSON(response, result); |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 设置当前用户 |
| | | if (loginUser != null) { |
| | | SecurityFrameworkUtils.setLoginUser(loginUser, request); |
| | | } |
| | | // 继续过滤链 |
| | | chain.doFilter(request, response); |
| | | } |
| | | |
| | | private LoginUser buildLoginUserByToken(String token, Integer userType) { |
| | | try { |
| | | // 校验访问令牌 |
| | | OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token).getCheckedData(); |
| | | if (accessToken == null) { |
| | | return null; |
| | | } |
| | | // 用户类型不匹配,无权限 |
| | | // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型 |
| | | // 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的 |
| | | if (userType != null |
| | | && ObjectUtil.notEqual(accessToken.getUserType(), userType)) { |
| | | throw new AccessDeniedException("错误的用户类型"); |
| | | } |
| | | // 构建登录用户 |
| | | return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) |
| | | .setInfo(accessToken.getUserInfo()) // 额外的用户信息 |
| | | .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) |
| | | .setExpiresTime(accessToken.getExpiresTime()); |
| | | } catch (ServiceException serviceException) { |
| | | // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 模拟登录用户,方便日常开发调试 |
| | | * |
| | | * 注意,在线上环境下,一定要关闭该功能!!! |
| | | * |
| | | * @param request 请求 |
| | | * @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号 |
| | | * @param userType 用户类型 |
| | | * @return 模拟的 LoginUser |
| | | */ |
| | | private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) { |
| | | if (!securityProperties.getMockEnable()) { |
| | | return null; |
| | | } |
| | | // 必须以 mockSecret 开头 |
| | | if (!token.startsWith(securityProperties.getMockSecret())) { |
| | | return null; |
| | | } |
| | | // 构建模拟用户 |
| | | Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length())); |
| | | return new LoginUser().setId(userId).setUserType(userType) |
| | | .setTenantId(WebFrameworkUtils.getTenantId(request)); |
| | | } |
| | | |
| | | @SneakyThrows |
| | | private LoginUser buildLoginUserByHeader(HttpServletRequest request) { |
| | | String loginUserStr = request.getHeader(SecurityFrameworkUtils.LOGIN_USER_HEADER); |
| | | if (StrUtil.isEmpty(loginUserStr)) { |
| | | return null; |
| | | } |
| | | try { |
| | | loginUserStr = URLDecoder.decode(loginUserStr, StandardCharsets.UTF_8.name()); // 解码,解决中文乱码问题 |
| | | return JsonUtils.parseObject(loginUserStr, LoginUser.class); |
| | | } catch (Exception ex) { |
| | | log.error("[buildLoginUserByHeader][解析 LoginUser({}) 发生异常]", loginUserStr, ex); ; |
| | | throw ex; |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.core.handler; |
| | | |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import com.iailab.framework.common.pojo.CommonResult; |
| | | import com.iailab.framework.security.core.util.SecurityFrameworkUtils; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.security.access.AccessDeniedException; |
| | | import org.springframework.security.web.access.AccessDeniedHandler; |
| | | import org.springframework.security.web.access.ExceptionTranslationFilter; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | |
| | | import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; |
| | | import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; |
| | | |
| | | /** |
| | | * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 |
| | | * |
| | | * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | @SuppressWarnings("JavadocReference") |
| | | public class AccessDeniedHandlerImpl implements AccessDeniedHandler { |
| | | |
| | | @Override |
| | | public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) |
| | | throws IOException, ServletException { |
| | | // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 |
| | | log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), |
| | | SecurityFrameworkUtils.getLoginUserId(), e); |
| | | // 返回 403 |
| | | ServletUtils.writeJSON(response, CommonResult.error(FORBIDDEN)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.core.handler; |
| | | |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import com.iailab.framework.common.pojo.CommonResult; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.security.core.AuthenticationException; |
| | | import org.springframework.security.web.AuthenticationEntryPoint; |
| | | import org.springframework.security.web.access.ExceptionTranslationFilter; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | |
| | | import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; |
| | | |
| | | /** |
| | | * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 |
| | | * |
| | | * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | | @Slf4j |
| | | @SuppressWarnings("JavadocReference") // 忽略文档引用报错 |
| | | public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { |
| | | |
| | | @Override |
| | | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { |
| | | log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); |
| | | // 返回 401 |
| | | ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.core.rpc; |
| | | |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.security.core.LoginUser; |
| | | import com.iailab.framework.security.core.util.SecurityFrameworkUtils; |
| | | import feign.RequestInterceptor; |
| | | import feign.RequestTemplate; |
| | | import lombok.SneakyThrows; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.net.URLEncoder; |
| | | import java.nio.charset.StandardCharsets; |
| | | |
| | | /** |
| | | * LoginUser 的 RequestInterceptor 实现类:Feign 请求时,将 {@link LoginUser} 设置到 header 中,继续透传给被调用的服务 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class LoginUserRequestInterceptor implements RequestInterceptor { |
| | | |
| | | @Override |
| | | @SneakyThrows |
| | | public void apply(RequestTemplate requestTemplate) { |
| | | LoginUser user = SecurityFrameworkUtils.getLoginUser(); |
| | | if (user == null) { |
| | | return; |
| | | } |
| | | try { |
| | | String userStr = JsonUtils.toJsonString(user); |
| | | userStr = URLEncoder.encode(userStr, StandardCharsets.UTF_8.name()); // 编码,避免中文乱码 |
| | | requestTemplate.header(SecurityFrameworkUtils.LOGIN_USER_HEADER, userStr); |
| | | } catch (Exception ex) { |
| | | log.error("[apply][序列化 LoginUser({}) 发生异常]", user, ex); |
| | | throw ex; |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.core.service; |
| | | |
| | | /** |
| | | * Security 框架 Service 接口,定义权限相关的校验操作 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface SecurityFrameworkService { |
| | | |
| | | /** |
| | | * 判断是否有权限 |
| | | * |
| | | * @param permission 权限 |
| | | * @return 是否 |
| | | */ |
| | | boolean hasPermission(String permission); |
| | | |
| | | /** |
| | | * 判断是否有权限,任一一个即可 |
| | | * |
| | | * @param permissions 权限 |
| | | * @return 是否 |
| | | */ |
| | | boolean hasAnyPermissions(String... permissions); |
| | | |
| | | /** |
| | | * 判断是否有角色 |
| | | * |
| | | * 注意,角色使用的是 SysRoleDO 的 code 标识 |
| | | * |
| | | * @param role 角色 |
| | | * @return 是否 |
| | | */ |
| | | boolean hasRole(String role); |
| | | |
| | | /** |
| | | * 判断是否有角色,任一一个即可 |
| | | * |
| | | * @param roles 角色数组 |
| | | * @return 是否 |
| | | */ |
| | | boolean hasAnyRoles(String... roles); |
| | | |
| | | /** |
| | | * 判断是否有授权 |
| | | * |
| | | * @param scope 授权 |
| | | * @return 是否 |
| | | */ |
| | | boolean hasScope(String scope); |
| | | |
| | | /** |
| | | * 判断是否有授权范围,任一一个即可 |
| | | * |
| | | * @param scope 授权范围数组 |
| | | * @return 是否 |
| | | */ |
| | | boolean hasAnyScopes(String... scope); |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.core.service; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.iailab.framework.common.core.KeyValue; |
| | | import com.iailab.framework.security.core.LoginUser; |
| | | import com.iailab.framework.security.core.util.SecurityFrameworkUtils; |
| | | import com.iailab.module.system.api.permission.PermissionApi; |
| | | import com.google.common.cache.CacheLoader; |
| | | import com.google.common.cache.LoadingCache; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.SneakyThrows; |
| | | |
| | | import java.time.Duration; |
| | | import java.util.Arrays; |
| | | import java.util.List; |
| | | |
| | | import static com.iailab.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; |
| | | import static com.iailab.framework.common.util.cache.CacheUtils.buildCache; |
| | | import static com.iailab.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | |
| | | /** |
| | | * 默认的 {@link SecurityFrameworkService} 实现类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AllArgsConstructor |
| | | public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { |
| | | |
| | | private final PermissionApi permissionApi; |
| | | |
| | | /** |
| | | * 针对 {@link #hasAnyRoles(String...)} 的缓存 |
| | | */ |
| | | private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyRolesCache = buildCache( |
| | | Duration.ofMinutes(1L), // 过期时间 1 分钟 |
| | | new CacheLoader<KeyValue<Long, List<String>>, Boolean>() { |
| | | |
| | | @Override |
| | | public Boolean load(KeyValue<Long, List<String>> key) { |
| | | return permissionApi.hasAnyRoles(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData(); |
| | | } |
| | | |
| | | }); |
| | | |
| | | /** |
| | | * 针对 {@link #hasAnyPermissions(String...)} 的缓存 |
| | | */ |
| | | private final LoadingCache<KeyValue<Long, List<String>>, Boolean> hasAnyPermissionsCache = buildCache( |
| | | Duration.ofMinutes(1L), // 过期时间 1 分钟 |
| | | new CacheLoader<KeyValue<Long, List<String>>, Boolean>() { |
| | | |
| | | @Override |
| | | public Boolean load(KeyValue<Long, List<String>> key) { |
| | | return permissionApi.hasAnyPermissions(key.getKey(), key.getValue().toArray(new String[0])).getCheckedData(); |
| | | } |
| | | |
| | | }); |
| | | |
| | | @Override |
| | | public boolean hasPermission(String permission) { |
| | | return hasAnyPermissions(permission); |
| | | } |
| | | |
| | | @Override |
| | | @SneakyThrows |
| | | public boolean hasAnyPermissions(String... permissions) { |
| | | Long userId = getLoginUserId(); |
| | | if (userId == null) { |
| | | return false; |
| | | } |
| | | return hasAnyPermissionsCache.get(new KeyValue<>(userId, Arrays.asList(permissions))); |
| | | } |
| | | |
| | | @Override |
| | | public boolean hasRole(String role) { |
| | | return hasAnyRoles(role); |
| | | } |
| | | |
| | | @Override |
| | | @SneakyThrows |
| | | public boolean hasAnyRoles(String... roles) { |
| | | Long userId = getLoginUserId(); |
| | | if (userId == null) { |
| | | return false; |
| | | } |
| | | return hasAnyRolesCache.get(new KeyValue<>(userId, Arrays.asList(roles))); |
| | | } |
| | | |
| | | @Override |
| | | public boolean hasScope(String scope) { |
| | | return hasAnyScopes(scope); |
| | | } |
| | | |
| | | @Override |
| | | public boolean hasAnyScopes(String... scope) { |
| | | LoginUser user = SecurityFrameworkUtils.getLoginUser(); |
| | | if (user == null) { |
| | | return false; |
| | | } |
| | | return CollUtil.containsAny(user.getScopes(), Arrays.asList(scope)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.security.core.util; |
| | | |
| | | import cn.hutool.core.map.MapUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.security.core.LoginUser; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import org.springframework.lang.Nullable; |
| | | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
| | | import org.springframework.security.core.Authentication; |
| | | import org.springframework.security.core.context.SecurityContext; |
| | | import org.springframework.security.core.context.SecurityContextHolder; |
| | | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.util.Collections; |
| | | |
| | | /** |
| | | * 安全服务工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class SecurityFrameworkUtils { |
| | | |
| | | /** |
| | | * HEADER 认证头 value 的前缀 |
| | | */ |
| | | public static final String AUTHORIZATION_BEARER = "Bearer"; |
| | | |
| | | public static final String LOGIN_USER_HEADER = "login-user"; |
| | | |
| | | private SecurityFrameworkUtils() {} |
| | | |
| | | /** |
| | | * 从请求中,获得认证 Token |
| | | * |
| | | * @param request 请求 |
| | | * @param headerName 认证 Token 对应的 Header 名字 |
| | | * @param parameterName 认证 Token 对应的 Parameter 名字 |
| | | * @return 认证 Token |
| | | */ |
| | | public static String obtainAuthorization(HttpServletRequest request, |
| | | String headerName, String parameterName) { |
| | | // 1. 获得 Token。优先级:Header > Parameter |
| | | String token = request.getHeader(headerName); |
| | | if (StrUtil.isEmpty(token)) { |
| | | token = request.getParameter(parameterName); |
| | | } |
| | | if (!StringUtils.hasText(token)) { |
| | | return null; |
| | | } |
| | | // 2. 去除 Token 中带的 Bearer |
| | | int index = token.indexOf(AUTHORIZATION_BEARER + " "); |
| | | return index >= 0 ? token.substring(index + 7).trim() : token; |
| | | } |
| | | |
| | | /** |
| | | * 获得当前认证信息 |
| | | * |
| | | * @return 认证信息 |
| | | */ |
| | | public static Authentication getAuthentication() { |
| | | SecurityContext context = SecurityContextHolder.getContext(); |
| | | if (context == null) { |
| | | return null; |
| | | } |
| | | return context.getAuthentication(); |
| | | } |
| | | |
| | | /** |
| | | * 获取当前用户 |
| | | * |
| | | * @return 当前用户 |
| | | */ |
| | | @Nullable |
| | | public static LoginUser getLoginUser() { |
| | | Authentication authentication = getAuthentication(); |
| | | if (authentication == null) { |
| | | return null; |
| | | } |
| | | return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; |
| | | } |
| | | |
| | | /** |
| | | * 获得当前用户的编号,从上下文中 |
| | | * |
| | | * @return 用户编号 |
| | | */ |
| | | @Nullable |
| | | public static Long getLoginUserId() { |
| | | LoginUser loginUser = getLoginUser(); |
| | | return loginUser != null ? loginUser.getId() : null; |
| | | } |
| | | |
| | | /** |
| | | * 获得当前用户的昵称,从上下文中 |
| | | * |
| | | * @return 昵称 |
| | | */ |
| | | @Nullable |
| | | public static String getLoginUserNickname() { |
| | | LoginUser loginUser = getLoginUser(); |
| | | return loginUser != null ? MapUtil.getStr(loginUser.getInfo(), LoginUser.INFO_KEY_NICKNAME) : null; |
| | | } |
| | | |
| | | /** |
| | | * 获得当前用户的部门编号,从上下文中 |
| | | * |
| | | * @return 部门编号 |
| | | */ |
| | | @Nullable |
| | | public static Long getLoginUserDeptId() { |
| | | LoginUser loginUser = getLoginUser(); |
| | | return loginUser != null ? MapUtil.getLong(loginUser.getInfo(), LoginUser.INFO_KEY_DEPT_ID) : null; |
| | | } |
| | | |
| | | /** |
| | | * 设置当前用户 |
| | | * |
| | | * @param loginUser 登录用户 |
| | | * @param request 请求 |
| | | */ |
| | | public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { |
| | | // 创建 Authentication,并设置到上下文 |
| | | Authentication authentication = buildAuthentication(loginUser, request); |
| | | SecurityContextHolder.getContext().setAuthentication(authentication); |
| | | |
| | | // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号; |
| | | // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 |
| | | WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); |
| | | WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); |
| | | } |
| | | |
| | | private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { |
| | | // 创建 UsernamePasswordAuthenticationToken 对象 |
| | | UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( |
| | | loginUser, null, Collections.emptyList()); |
| | | authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); |
| | | return authenticationToken; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 基于 Spring Security 框架 |
| | | * 实现安全认证功能 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | package com.iailab.framework.security; |
对比新文件 |
| | |
| | | com.iailab.framework.security.config.IailabSecurityRpcAutoConfiguration |
| | | com.iailab.framework.security.config.IailabSecurityAutoConfiguration |
| | | com.iailab.framework.security.config.IailabWebSecurityConfigurerAdapter |
| | | com.iailab.framework.operatelog.config.IailabOperateLogConfiguration |
| | | com.iailab.framework.operatelog.config.IailabOperateLogRpcAutoConfiguration |
对比新文件 |
| | |
| | | <assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd"> |
| | | <!-- <id>jar-with-dependencies</id>--> |
| | | <formats> |
| | | <format>jar</format> |
| | | </formats> |
| | | <includeBaseDirectory>true</includeBaseDirectory> |
| | | |
| | | <!--依赖jar包以及项目打包文件存储文件--> |
| | | <dependencySets> |
| | | <dependencySet> |
| | | <!--存储在projectName-assembly-version/lib下--> |
| | | <outputDirectory>lib</outputDirectory> |
| | | </dependencySet> |
| | | </dependencySets> |
| | | |
| | | <files> |
| | | <file> |
| | | <fileMode>775</fileMode> |
| | | <source>target/${project.build.finalName}.jar</source> |
| | | <destName>demo-maven-assembly.jar</destName> |
| | | <outputDirectory>./target</outputDirectory> |
| | | </file> |
| | | </files> |
| | | |
| | | <fileSets> |
| | | <fileSet> |
| | | <directory>src/main/resources</directory> |
| | | <outputDirectory>./conf</outputDirectory> |
| | | <includes> |
| | | <include>*.yml</include> |
| | | <include>*.properties</include> |
| | | </includes> |
| | | </fileSet> |
| | | <fileSet> |
| | | <fileMode>775</fileMode> |
| | | <directory>deploy/bin</directory> |
| | | <outputDirectory>./bin</outputDirectory> |
| | | </fileSet> |
| | | <fileSet> |
| | | <directory>${basedir}</directory> |
| | | <includes> |
| | | <include>*.md</include> |
| | | </includes> |
| | | </fileSet> |
| | | |
| | | </fileSets> |
| | | |
| | | </assembly> |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-test</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>测试组件,用于单元测试、集成测试</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- DB 相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-mybatis</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-redis</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Test 测试相关 --> |
| | | <dependency> |
| | | <groupId>org.mockito</groupId> |
| | | <artifactId>mockito-inline</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-test</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.h2database</groupId> <!-- 单元测试,我们采用 H2 作为数据库 --> |
| | | <artifactId>h2</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 --> |
| | | <artifactId>jedis-mock</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 --> |
| | | <artifactId>podam</artifactId> |
| | | </dependency> |
| | | </dependencies> |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.test.config; |
| | | |
| | | import com.github.fppt.jedismock.RedisServer; |
| | | import org.springframework.boot.autoconfigure.data.redis.RedisProperties; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.context.annotation.Lazy; |
| | | |
| | | import java.io.IOException; |
| | | |
| | | /** |
| | | * Redis 测试 Configuration,主要实现内嵌 Redis 的启动 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Configuration(proxyBeanMethods = false) |
| | | @Lazy(false) // 禁止延迟加载 |
| | | @EnableConfigurationProperties(RedisProperties.class) |
| | | public class RedisTestConfiguration { |
| | | |
| | | /** |
| | | * 创建模拟的 Redis Server 服务器 |
| | | */ |
| | | @Bean |
| | | public RedisServer redisServer(RedisProperties properties) throws IOException { |
| | | RedisServer redisServer = new RedisServer(properties.getPort()); |
| | | // 一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。 |
| | | try { |
| | | redisServer.start(); |
| | | } catch (Exception ignore) {} |
| | | return redisServer; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.test.config; |
| | | |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; |
| | | import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; |
| | | import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer; |
| | | import org.springframework.boot.sql.init.DatabaseInitializationSettings; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.context.annotation.Lazy; |
| | | |
| | | import javax.sql.DataSource; |
| | | |
| | | /** |
| | | * SQL 初始化的测试 Configuration |
| | | * |
| | | * 为什么不使用 org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration 呢? |
| | | * 因为我们在单元测试会使用 spring.main.lazy-initialization 为 true,开启延迟加载。此时,会导致 DataSourceInitializationConfiguration 初始化 |
| | | * 不过呢,当前类的实现代码,基本是复制 DataSourceInitializationConfiguration 的哈! |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Configuration(proxyBeanMethods = false) |
| | | @ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class) |
| | | @ConditionalOnSingleCandidate(DataSource.class) |
| | | @ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator") |
| | | @Lazy(value = false) // 禁止延迟加载 |
| | | @EnableConfigurationProperties(SqlInitializationProperties.class) |
| | | public class SqlInitializationTestConfiguration { |
| | | |
| | | @Bean |
| | | public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource, |
| | | SqlInitializationProperties initializationProperties) { |
| | | DatabaseInitializationSettings settings = createFrom(initializationProperties); |
| | | return new DataSourceScriptDatabaseInitializer(dataSource, settings); |
| | | } |
| | | |
| | | static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) { |
| | | DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); |
| | | settings.setSchemaLocations(properties.getSchemaLocations()); |
| | | settings.setDataLocations(properties.getDataLocations()); |
| | | settings.setContinueOnError(properties.isContinueOnError()); |
| | | settings.setSeparator(properties.getSeparator()); |
| | | settings.setEncoding(properties.getEncoding()); |
| | | settings.setMode(properties.getMode()); |
| | | return settings; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.test.core.ut; |
| | | |
| | | import com.iailab.framework.datasource.config.IailabDataSourceAutoConfiguration; |
| | | import com.iailab.framework.mybatis.config.IailabMybatisAutoConfiguration; |
| | | import com.iailab.framework.redis.config.IailabRedisAutoConfiguration; |
| | | import com.iailab.framework.test.config.RedisTestConfiguration; |
| | | import com.iailab.framework.test.config.SqlInitializationTestConfiguration; |
| | | import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure; |
| | | import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; |
| | | import org.redisson.spring.starter.RedissonAutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; |
| | | import org.springframework.boot.test.context.SpringBootTest; |
| | | import org.springframework.context.annotation.Import; |
| | | import org.springframework.test.context.ActiveProfiles; |
| | | import org.springframework.test.context.jdbc.Sql; |
| | | |
| | | /** |
| | | * 依赖内存 DB + Redis 的单元测试 |
| | | * |
| | | * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class) |
| | | @ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 |
| | | @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB |
| | | public class BaseDbAndRedisUnitTest { |
| | | |
| | | @Import({ |
| | | // DB 配置类 |
| | | IailabDataSourceAutoConfiguration.class, // 自己的 DB 配置类 |
| | | DataSourceAutoConfiguration.class, // Spring DB 自动配置类 |
| | | DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 |
| | | DruidDataSourceAutoConfigure.class, // Druid 自动配置类 |
| | | SqlInitializationTestConfiguration.class, // SQL 初始化 |
| | | // MyBatis 配置类 |
| | | IailabMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 |
| | | MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 |
| | | |
| | | // Redis 配置类 |
| | | RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer |
| | | IailabRedisAutoConfiguration.class, // 自己的 Redis 配置类 |
| | | RedisAutoConfiguration.class, // Spring Redis 自动配置类 |
| | | RedissonAutoConfiguration.class, // Redisson 自动配置类 |
| | | }) |
| | | public static class Application { |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.test.core.ut; |
| | | |
| | | import com.iailab.framework.datasource.config.IailabDataSourceAutoConfiguration; |
| | | import com.iailab.framework.mybatis.config.IailabMybatisAutoConfiguration; |
| | | import com.iailab.framework.test.config.SqlInitializationTestConfiguration; |
| | | import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure; |
| | | import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; |
| | | import com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; |
| | | import org.springframework.boot.test.context.SpringBootTest; |
| | | import org.springframework.context.annotation.Import; |
| | | import org.springframework.test.context.ActiveProfiles; |
| | | import org.springframework.test.context.jdbc.Sql; |
| | | |
| | | /** |
| | | * 依赖内存 DB 的单元测试 |
| | | * |
| | | * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class) |
| | | @ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 |
| | | @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB |
| | | public class BaseDbUnitTest { |
| | | |
| | | @Import({ |
| | | // DB 配置类 |
| | | IailabDataSourceAutoConfiguration.class, // 自己的 DB 配置类 |
| | | DataSourceAutoConfiguration.class, // Spring DB 自动配置类 |
| | | DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 |
| | | DruidDataSourceAutoConfigure.class, // Druid 自动配置类 |
| | | SqlInitializationTestConfiguration.class, // SQL 初始化 |
| | | // MyBatis 配置类 |
| | | IailabMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 |
| | | MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 |
| | | MybatisPlusJoinAutoConfiguration.class, // MyBatis 的Join配置类 |
| | | }) |
| | | public static class Application { |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.test.core.ut; |
| | | |
| | | import org.junit.jupiter.api.extension.ExtendWith; |
| | | import org.mockito.junit.jupiter.MockitoExtension; |
| | | |
| | | /** |
| | | * 纯 Mockito 的单元测试 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @ExtendWith(MockitoExtension.class) |
| | | public class BaseMockitoUnitTest { |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.test.core.ut; |
| | | |
| | | import com.iailab.framework.redis.config.IailabRedisAutoConfiguration; |
| | | import com.iailab.framework.test.config.RedisTestConfiguration; |
| | | import org.redisson.spring.starter.RedissonAutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; |
| | | import org.springframework.boot.test.context.SpringBootTest; |
| | | import org.springframework.context.annotation.Import; |
| | | import org.springframework.test.context.ActiveProfiles; |
| | | |
| | | /** |
| | | * 依赖内存 Redis 的单元测试 |
| | | * |
| | | * 相比 {@link BaseDbUnitTest} 来说,从内存 DB 改成了内存 Redis |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisUnitTest.Application.class) |
| | | @ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 |
| | | public class BaseRedisUnitTest { |
| | | |
| | | @Import({ |
| | | // Redis 配置类 |
| | | RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer |
| | | RedisAutoConfiguration.class, // Spring Redis 自动配置类 |
| | | IailabRedisAutoConfiguration.class, // 自己的 Redis 配置类 |
| | | RedissonAutoConfiguration.class, // Redisson 自动配置类 |
| | | }) |
| | | public static class Application { |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 提供单元测试 Unit Test 的基类 |
| | | */ |
| | | package com.iailab.framework.test.core.ut; |
对比新文件 |
| | |
| | | package com.iailab.framework.test.core.util; |
| | | |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import cn.hutool.core.util.ReflectUtil; |
| | | import com.iailab.framework.common.exception.ErrorCode; |
| | | import com.iailab.framework.common.exception.ServiceException; |
| | | import com.iailab.framework.common.exception.util.ServiceExceptionUtil; |
| | | import org.junit.jupiter.api.Assertions; |
| | | import org.junit.jupiter.api.function.Executable; |
| | | |
| | | import java.lang.reflect.Field; |
| | | import java.util.Arrays; |
| | | import java.util.Objects; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | |
| | | /** |
| | | * 单元测试,assert 断言工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class AssertUtils { |
| | | |
| | | /** |
| | | * 比对两个对象的属性是否一致 |
| | | * |
| | | * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 |
| | | * |
| | | * @param expected 期望对象 |
| | | * @param actual 实际对象 |
| | | * @param ignoreFields 忽略的属性数组 |
| | | */ |
| | | public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) { |
| | | Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); |
| | | Arrays.stream(expectedFields).forEach(expectedField -> { |
| | | // 忽略 jacoco 自动生成的 $jacocoData 属性的情况 |
| | | if (expectedField.isSynthetic()) { |
| | | return; |
| | | } |
| | | // 如果是忽略的属性,则不进行比对 |
| | | if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { |
| | | return; |
| | | } |
| | | // 忽略不存在的属性 |
| | | Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); |
| | | if (actualField == null) { |
| | | return; |
| | | } |
| | | // 比对 |
| | | Assertions.assertEquals( |
| | | ReflectUtil.getFieldValue(expected, expectedField), |
| | | ReflectUtil.getFieldValue(actual, actualField), |
| | | String.format("Field(%s) 不匹配", expectedField.getName()) |
| | | ); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 比对两个对象的属性是否一致 |
| | | * |
| | | * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 |
| | | * |
| | | * @param expected 期望对象 |
| | | * @param actual 实际对象 |
| | | * @param ignoreFields 忽略的属性数组 |
| | | * @return 是否一致 |
| | | */ |
| | | public static boolean isPojoEquals(Object expected, Object actual, String... ignoreFields) { |
| | | Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); |
| | | return Arrays.stream(expectedFields).allMatch(expectedField -> { |
| | | // 如果是忽略的属性,则不进行比对 |
| | | if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { |
| | | return true; |
| | | } |
| | | // 忽略不存在的属性 |
| | | Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); |
| | | if (actualField == null) { |
| | | return true; |
| | | } |
| | | return Objects.equals(ReflectUtil.getFieldValue(expected, expectedField), |
| | | ReflectUtil.getFieldValue(actual, actualField)); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 执行方法,校验抛出的 Service 是否符合条件 |
| | | * |
| | | * @param executable 业务异常 |
| | | * @param errorCode 错误码对象 |
| | | * @param messageParams 消息参数 |
| | | */ |
| | | public static void assertServiceException(Executable executable, ErrorCode errorCode, Object... messageParams) { |
| | | // 调用方法 |
| | | ServiceException serviceException = assertThrows(ServiceException.class, executable); |
| | | // 校验错误码 |
| | | Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配"); |
| | | String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams); |
| | | Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配"); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.test.core.util; |
| | | |
| | | import cn.hutool.core.date.LocalDateTimeUtil; |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import cn.hutool.core.util.RandomUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.enums.CommonStatusEnum; |
| | | import uk.co.jemos.podam.api.PodamFactory; |
| | | import uk.co.jemos.podam.api.PodamFactoryImpl; |
| | | |
| | | import java.lang.reflect.Type; |
| | | import java.time.LocalDateTime; |
| | | import java.util.Arrays; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.function.Consumer; |
| | | import java.util.stream.Collectors; |
| | | import java.util.stream.Stream; |
| | | |
| | | /** |
| | | * 随机工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class RandomUtils { |
| | | |
| | | private static final int RANDOM_STRING_LENGTH = 10; |
| | | |
| | | private static final int TINYINT_MAX = 127; |
| | | |
| | | private static final int RANDOM_DATE_MAX = 30; |
| | | |
| | | private static final int RANDOM_COLLECTION_LENGTH = 5; |
| | | |
| | | private static final PodamFactory PODAM_FACTORY = new PodamFactoryImpl(); |
| | | |
| | | static { |
| | | // 字符串 |
| | | PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(String.class, |
| | | (dataProviderStrategy, attributeMetadata, map) -> randomString()); |
| | | // Integer |
| | | PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Integer.class, (dataProviderStrategy, attributeMetadata, map) -> { |
| | | // 如果是 status 的字段,返回 0 或 1 |
| | | if ("status".equals(attributeMetadata.getAttributeName())) { |
| | | return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); |
| | | } |
| | | // 如果是 type、status 结尾的字段,返回 tinyint 范围 |
| | | if (StrUtil.endWithAnyIgnoreCase(attributeMetadata.getAttributeName(), |
| | | "type", "status", "category", "scope", "result")) { |
| | | return RandomUtil.randomInt(0, TINYINT_MAX + 1); |
| | | } |
| | | return RandomUtil.randomInt(); |
| | | }); |
| | | // LocalDateTime |
| | | PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class, |
| | | (dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime()); |
| | | // Boolean |
| | | PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Boolean.class, (dataProviderStrategy, attributeMetadata, map) -> { |
| | | // 如果是 deleted 的字段,返回非删除 |
| | | if ("deleted".equals(attributeMetadata.getAttributeName())) { |
| | | return false; |
| | | } |
| | | return RandomUtil.randomBoolean(); |
| | | }); |
| | | } |
| | | |
| | | public static String randomString() { |
| | | return RandomUtil.randomString(RANDOM_STRING_LENGTH); |
| | | } |
| | | |
| | | public static Long randomLongId() { |
| | | return RandomUtil.randomLong(0, Long.MAX_VALUE); |
| | | } |
| | | |
| | | public static Integer randomInteger() { |
| | | return RandomUtil.randomInt(0, Integer.MAX_VALUE); |
| | | } |
| | | |
| | | public static Date randomDate() { |
| | | return RandomUtil.randomDay(0, RANDOM_DATE_MAX); |
| | | } |
| | | |
| | | public static LocalDateTime randomLocalDateTime() { |
| | | // 设置 Nano 为零的原因,避免 MySQL、H2 存储不到时间戳 |
| | | return LocalDateTimeUtil.of(randomDate()).withNano(0); |
| | | } |
| | | |
| | | public static Short randomShort() { |
| | | return (short) RandomUtil.randomInt(0, Short.MAX_VALUE); |
| | | } |
| | | |
| | | public static <T> Set<T> randomSet(Class<T> clazz) { |
| | | return Stream.iterate(0, i -> i).limit(RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH)) |
| | | .map(i -> randomPojo(clazz)).collect(Collectors.toSet()); |
| | | } |
| | | |
| | | public static Integer randomCommonStatus() { |
| | | return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); |
| | | } |
| | | |
| | | public static String randomEmail() { |
| | | return randomString() + "@qq.com"; |
| | | } |
| | | |
| | | public static String randomURL() { |
| | | return "https://www.baidu.com/" + randomString(); |
| | | } |
| | | |
| | | @SafeVarargs |
| | | public static <T> T randomPojo(Class<T> clazz, Consumer<T>... consumers) { |
| | | T pojo = PODAM_FACTORY.manufacturePojo(clazz); |
| | | // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 |
| | | if (ArrayUtil.isNotEmpty(consumers)) { |
| | | Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); |
| | | } |
| | | return pojo; |
| | | } |
| | | |
| | | @SafeVarargs |
| | | public static <T> T randomPojo(Class<T> clazz, Type type, Consumer<T>... consumers) { |
| | | T pojo = PODAM_FACTORY.manufacturePojo(clazz, type); |
| | | // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 |
| | | if (ArrayUtil.isNotEmpty(consumers)) { |
| | | Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); |
| | | } |
| | | return pojo; |
| | | } |
| | | |
| | | @SafeVarargs |
| | | public static <T> List<T> randomPojoList(Class<T> clazz, Consumer<T>... consumers) { |
| | | int size = RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH); |
| | | return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers)) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 测试组件,用于单元测试、集成测试等等 |
| | | */ |
| | | package com.iailab.framework.test; |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-web</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>Web 框架,全局异常、API 日志、脱敏、错误码等</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.baomidou</groupId> |
| | | <artifactId>mybatis-plus-boot-starter</artifactId> <!-- 捕获mybatis全局异常 --> |
| | | </dependency> |
| | | |
| | | <!-- Spring Boot 配置所需依赖 --> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-configuration-processor</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <!-- Web 相关 --> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-web</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-validation</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.springframework.security</groupId> |
| | | <artifactId>spring-security-core</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,主要是 GlobalExceptionHandler 使用 –>--> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.github.xiaoymin</groupId> <!-- 接口文档 --> |
| | | <artifactId>knife4j-openapi3-spring-boot-starter</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springdoc</groupId> <!-- 接口文档 --> |
| | | <artifactId>springdoc-openapi-ui</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- RPC 远程调用相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-rpc</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <!-- 业务组件 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-module-infra-api</artifactId> <!-- 需要使用它,进行操作日志的记录 --> |
| | | <version>${revision}</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-module-system-api</artifactId> <!-- 需要使用它,进行错误码的记录 --> |
| | | <version>${revision}</version> |
| | | </dependency> |
| | | |
| | | <!-- xss --> |
| | | <dependency> |
| | | <groupId>org.jsoup</groupId> |
| | | <artifactId>jsoup</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Test 测试相关 --> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-test</artifactId> |
| | | <scope>test</scope> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.mockito</groupId> |
| | | <artifactId>mockito-inline</artifactId> |
| | | <scope>test</scope> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.apilog.config; |
| | | |
| | | import com.iailab.framework.apilog.core.filter.ApiAccessLogFilter; |
| | | import com.iailab.framework.apilog.core.interceptor.ApiAccessLogInterceptor; |
| | | import com.iailab.framework.apilog.core.service.ApiAccessLogFrameworkService; |
| | | import com.iailab.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl; |
| | | import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkService; |
| | | import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl; |
| | | import com.iailab.framework.common.enums.WebFilterOrderEnum; |
| | | import com.iailab.framework.web.config.WebProperties; |
| | | import com.iailab.framework.web.config.IailabWebAutoConfiguration; |
| | | import com.iailab.module.infra.api.logger.ApiAccessLogApi; |
| | | import com.iailab.module.infra.api.logger.ApiErrorLogApi; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; |
| | | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; |
| | | |
| | | import javax.servlet.Filter; |
| | | |
| | | @AutoConfiguration(after = IailabWebAutoConfiguration.class) |
| | | public class IailabApiLogAutoConfiguration implements WebMvcConfigurer { |
| | | |
| | | @Bean |
| | | @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") |
| | | public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) { |
| | | return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi); |
| | | } |
| | | |
| | | @Bean |
| | | @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") |
| | | public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) { |
| | | return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi); |
| | | } |
| | | |
| | | /** |
| | | * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 |
| | | */ |
| | | @Bean |
| | | @ConditionalOnProperty(prefix = "iailab.access-log", value = "enable", matchIfMissing = true) // 允许使用 iailab.access-log.enable=false 禁用访问日志 |
| | | public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties, |
| | | @Value("${spring.application.name}") String applicationName, |
| | | ApiAccessLogFrameworkService apiAccessLogFrameworkService) { |
| | | ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService); |
| | | return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER); |
| | | } |
| | | |
| | | private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) { |
| | | FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter); |
| | | bean.setOrder(order); |
| | | return bean; |
| | | } |
| | | |
| | | @Override |
| | | public void addInterceptors(InterceptorRegistry registry) { |
| | | registry.addInterceptor(new ApiAccessLogInterceptor()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.apilog.config; |
| | | |
| | | import com.iailab.module.infra.api.logger.ApiAccessLogApi; |
| | | import com.iailab.module.infra.api.logger.ApiErrorLogApi; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.cloud.openfeign.EnableFeignClients; |
| | | import org.springframework.context.annotation.Configuration; |
| | | |
| | | /** |
| | | * API 日志使用到 Feign 的配置项 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @EnableFeignClients(clients = {ApiAccessLogApi.class, // 主要是引入相关的 API 服务 |
| | | ApiErrorLogApi.class}) |
| | | public class IailabApiLogRpcAutoConfiguration { |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.apilog.core.annotation; |
| | | |
| | | import com.iailab.framework.apilog.core.enums.OperateTypeEnum; |
| | | |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 访问日志注解 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Target({ElementType.METHOD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | public @interface ApiAccessLog { |
| | | |
| | | // ========== 开关字段 ========== |
| | | |
| | | /** |
| | | * 是否记录访问日志 |
| | | */ |
| | | boolean enable() default true; |
| | | /** |
| | | * 是否记录请求参数 |
| | | * |
| | | * 默认记录,主要考虑请求数据一般不大。可手动设置为 false 进行关闭 |
| | | */ |
| | | boolean requestEnable() default true; |
| | | /** |
| | | * 是否记录响应结果 |
| | | * |
| | | * 默认不记录,主要考虑响应数据可能比较大。可手动设置为 true 进行打开 |
| | | */ |
| | | boolean responseEnable() default false; |
| | | /** |
| | | * 敏感参数数组 |
| | | * |
| | | * 添加后,请求参数、响应结果不会记录该参数 |
| | | */ |
| | | String[] sanitizeKeys() default {}; |
| | | |
| | | // ========== 模块字段 ========== |
| | | |
| | | /** |
| | | * 操作模块 |
| | | * |
| | | * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.tags.Tag#name()} 属性 |
| | | */ |
| | | String operateModule() default ""; |
| | | /** |
| | | * 操作名 |
| | | * |
| | | * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.Operation#summary()} 属性 |
| | | */ |
| | | String operateName() default ""; |
| | | /** |
| | | * 操作分类 |
| | | * |
| | | * 实际并不是数组,因为枚举不能设置 null 作为默认值 |
| | | */ |
| | | OperateTypeEnum[] operateType() default {}; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.apilog.core.enums; |
| | | |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Getter; |
| | | |
| | | /** |
| | | * 操作日志的操作类型 |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | | @Getter |
| | | @AllArgsConstructor |
| | | public enum OperateTypeEnum { |
| | | |
| | | /** |
| | | * 查询 |
| | | */ |
| | | GET(1), |
| | | /** |
| | | * 新增 |
| | | */ |
| | | CREATE(2), |
| | | /** |
| | | * 修改 |
| | | */ |
| | | UPDATE(3), |
| | | /** |
| | | * 删除 |
| | | */ |
| | | DELETE(4), |
| | | /** |
| | | * 导出 |
| | | */ |
| | | EXPORT(5), |
| | | /** |
| | | * 导入 |
| | | */ |
| | | IMPORT(6), |
| | | /** |
| | | * 其它 |
| | | * |
| | | * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识 |
| | | */ |
| | | OTHER(0); |
| | | |
| | | /** |
| | | * 类型 |
| | | */ |
| | | private final Integer type; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.apilog.core.filter; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.date.LocalDateTimeUtil; |
| | | import cn.hutool.core.exceptions.ExceptionUtil; |
| | | import cn.hutool.core.map.MapUtil; |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import cn.hutool.core.util.BooleanUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.apilog.core.annotation.ApiAccessLog; |
| | | import com.iailab.framework.apilog.core.enums.OperateTypeEnum; |
| | | import com.iailab.framework.apilog.core.service.ApiAccessLogFrameworkService; |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import com.iailab.framework.common.pojo.CommonResult; |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.common.util.monitor.TracerUtils; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import com.iailab.framework.web.config.WebProperties; |
| | | import com.iailab.framework.web.core.filter.ApiRequestFilter; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import com.iailab.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; |
| | | import com.fasterxml.jackson.databind.JsonNode; |
| | | import io.swagger.v3.oas.annotations.Operation; |
| | | import io.swagger.v3.oas.annotations.tags.Tag; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.web.bind.annotation.RequestMethod; |
| | | import org.springframework.web.method.HandlerMethod; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | import java.time.LocalDateTime; |
| | | import java.time.temporal.ChronoUnit; |
| | | import java.util.Iterator; |
| | | import java.util.Map; |
| | | |
| | | import static com.iailab.framework.apilog.core.interceptor.ApiAccessLogInterceptor.*; |
| | | import static com.iailab.framework.common.util.json.JsonUtils.toJsonString; |
| | | |
| | | /** |
| | | * API 访问日志 Filter |
| | | * |
| | | * 目的:记录 API 访问日志到数据库中 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class ApiAccessLogFilter extends ApiRequestFilter { |
| | | |
| | | private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"}; |
| | | |
| | | private final String applicationName; |
| | | |
| | | private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; |
| | | |
| | | public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) { |
| | | super(webProperties); |
| | | this.applicationName = applicationName; |
| | | this.apiAccessLogFrameworkService = apiAccessLogFrameworkService; |
| | | } |
| | | |
| | | @Override |
| | | @SuppressWarnings("NullableProblems") |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
| | | throws ServletException, IOException { |
| | | // 获得开始时间 |
| | | LocalDateTime beginTime = LocalDateTime.now(); |
| | | // 提前获得参数,避免 XssFilter 过滤处理 |
| | | Map<String, String> queryString = ServletUtils.getParamMap(request); |
| | | String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; |
| | | |
| | | try { |
| | | // 继续过滤器 |
| | | filterChain.doFilter(request, response); |
| | | // 正常执行,记录日志 |
| | | createApiAccessLog(request, beginTime, queryString, requestBody, null); |
| | | } catch (Exception ex) { |
| | | // 异常执行,记录日志 |
| | | createApiAccessLog(request, beginTime, queryString, requestBody, ex); |
| | | throw ex; |
| | | } |
| | | } |
| | | |
| | | private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime, |
| | | Map<String, String> queryString, String requestBody, Exception ex) { |
| | | ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO(); |
| | | try { |
| | | boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex); |
| | | if (!enable) { |
| | | return; |
| | | } |
| | | apiAccessLogFrameworkService.createApiAccessLog(accessLog); |
| | | } catch (Throwable th) { |
| | | log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th); |
| | | } |
| | | } |
| | | |
| | | private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime, |
| | | Map<String, String> queryString, String requestBody, Exception ex) { |
| | | // 判断:是否要记录操作日志 |
| | | HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ATTRIBUTE_HANDLER_METHOD); |
| | | ApiAccessLog accessLogAnnotation = null; |
| | | if (handlerMethod != null) { |
| | | accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class); |
| | | if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // 处理用户信息 |
| | | accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)) |
| | | .setUserType(WebFrameworkUtils.getLoginUserType(request)); |
| | | // 设置访问结果 |
| | | CommonResult<?> result = WebFrameworkUtils.getCommonResult(request); |
| | | if (result != null) { |
| | | accessLog.setResultCode(result.getCode()).setResultMsg(result.getMsg()); |
| | | } else if (ex != null) { |
| | | accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()) |
| | | .setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); |
| | | } else { |
| | | accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()).setResultMsg(""); |
| | | } |
| | | // 设置请求字段 |
| | | accessLog.setTraceId(TracerUtils.getTraceId()).setApplicationName(applicationName) |
| | | .setRequestUrl(request.getRequestURI()).setRequestMethod(request.getMethod()) |
| | | .setUserAgent(ServletUtils.getUserAgent(request)).setUserIp(ServletUtils.getClientIP(request)); |
| | | String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null; |
| | | Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE; |
| | | if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false |
| | | Map<String, Object> requestParams = MapUtil.<String, Object>builder() |
| | | .put("query", sanitizeMap(queryString, sanitizeKeys)) |
| | | .put("body", sanitizeJson(requestBody, sanitizeKeys)).build(); |
| | | accessLog.setRequestParams(toJsonString(requestParams)); |
| | | } |
| | | Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE; |
| | | if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true |
| | | accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys)); |
| | | } |
| | | // 持续时间 |
| | | accessLog.setBeginTime(beginTime).setEndTime(LocalDateTime.now()) |
| | | .setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS)); |
| | | |
| | | // 操作模块 |
| | | if (handlerMethod != null) { |
| | | Tag tagAnnotation = handlerMethod.getBeanType().getAnnotation(Tag.class); |
| | | Operation operationAnnotation = handlerMethod.getMethodAnnotation(Operation.class); |
| | | String operateModule = accessLogAnnotation != null ? accessLogAnnotation.operateModule() : |
| | | tagAnnotation != null ? StrUtil.nullToDefault(tagAnnotation.name(), tagAnnotation.description()) : null; |
| | | String operateName = accessLogAnnotation != null ? accessLogAnnotation.operateName() : |
| | | operationAnnotation != null ? operationAnnotation.summary() : null; |
| | | OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ? |
| | | accessLogAnnotation.operateType()[0] : parseOperateLogType(request); |
| | | accessLog.setOperateModule(operateModule).setOperateName(operateName).setOperateType(operateType.getType()); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | // ========== 解析 @ApiAccessLog、@Swagger 注解 ========== |
| | | |
| | | private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) { |
| | | RequestMethod requestMethod = ArrayUtil.firstMatch(method -> |
| | | StrUtil.equalsAnyIgnoreCase(method.name(), request.getMethod()), RequestMethod.values()); |
| | | if (requestMethod == null) { |
| | | return OperateTypeEnum.OTHER; |
| | | } |
| | | switch (requestMethod) { |
| | | case GET: |
| | | return OperateTypeEnum.GET; |
| | | case POST: |
| | | return OperateTypeEnum.CREATE; |
| | | case PUT: |
| | | return OperateTypeEnum.UPDATE; |
| | | case DELETE: |
| | | return OperateTypeEnum.DELETE; |
| | | default: |
| | | return OperateTypeEnum.OTHER; |
| | | } |
| | | } |
| | | |
| | | // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ========== |
| | | |
| | | private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) { |
| | | if (CollUtil.isNotEmpty(map)) { |
| | | return null; |
| | | } |
| | | if (sanitizeKeys != null) { |
| | | MapUtil.removeAny(map, sanitizeKeys); |
| | | } |
| | | MapUtil.removeAny(map, SANITIZE_KEYS); |
| | | return JsonUtils.toJsonString(map); |
| | | } |
| | | |
| | | private static String sanitizeJson(String jsonString, String[] sanitizeKeys) { |
| | | if (StrUtil.isEmpty(jsonString)) { |
| | | return null; |
| | | } |
| | | try { |
| | | JsonNode rootNode = JsonUtils.parseTree(jsonString); |
| | | sanitizeJson(rootNode, sanitizeKeys); |
| | | return JsonUtils.toJsonString(rootNode); |
| | | } catch (Exception e) { |
| | | // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 |
| | | log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); |
| | | return jsonString; |
| | | } |
| | | } |
| | | |
| | | private static String sanitizeJson(CommonResult<?> commonResult, String[] sanitizeKeys) { |
| | | if (commonResult == null) { |
| | | return null; |
| | | } |
| | | String jsonString = toJsonString(commonResult); |
| | | try { |
| | | JsonNode rootNode = JsonUtils.parseTree(jsonString); |
| | | sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉 |
| | | return JsonUtils.toJsonString(rootNode); |
| | | } catch (Exception e) { |
| | | // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 |
| | | log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); |
| | | return jsonString; |
| | | } |
| | | } |
| | | |
| | | private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) { |
| | | // 情况一:数组,遍历处理 |
| | | if (node.isArray()) { |
| | | for (JsonNode childNode : node) { |
| | | sanitizeJson(childNode, sanitizeKeys); |
| | | } |
| | | return; |
| | | } |
| | | // 情况二:非 Object,只是某个值,直接返回 |
| | | if (!node.isObject()) { |
| | | return; |
| | | } |
| | | // 情况三:Object,遍历处理 |
| | | Iterator<Map.Entry<String, JsonNode>> iterator = node.fields(); |
| | | while (iterator.hasNext()) { |
| | | Map.Entry<String, JsonNode> entry = iterator.next(); |
| | | if (ArrayUtil.contains(sanitizeKeys, entry.getKey()) |
| | | || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) { |
| | | iterator.remove(); |
| | | continue; |
| | | } |
| | | sanitizeJson(entry.getValue(), sanitizeKeys); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.apilog.core.interceptor; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.io.FileUtil; |
| | | import cn.hutool.core.io.resource.ResourceUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import com.iailab.framework.common.util.spring.SpringUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.util.StopWatch; |
| | | import org.springframework.web.method.HandlerMethod; |
| | | import org.springframework.web.servlet.HandlerInterceptor; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.lang.reflect.Method; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Optional; |
| | | import java.util.stream.IntStream; |
| | | |
| | | /** |
| | | * API 访问日志 Interceptor |
| | | * |
| | | * 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class ApiAccessLogInterceptor implements HandlerInterceptor { |
| | | |
| | | public static final String ATTRIBUTE_HANDLER_METHOD = "HANDLER_METHOD"; |
| | | |
| | | private static final String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch"; |
| | | |
| | | @Override |
| | | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { |
| | | // 记录 HandlerMethod,提供给 ApiAccessLogFilter 使用 |
| | | HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod) handler : null; |
| | | if (handlerMethod != null) { |
| | | request.setAttribute(ATTRIBUTE_HANDLER_METHOD, handlerMethod); |
| | | } |
| | | |
| | | // 打印 request 日志 |
| | | if (!SpringUtils.isProd()) { |
| | | Map<String, String> queryString = ServletUtils.getParamMap(request); |
| | | String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; |
| | | if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) { |
| | | log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI()); |
| | | } else { |
| | | log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(), |
| | | StrUtil.blankToDefault(requestBody, queryString.toString())); |
| | | } |
| | | // 计时 |
| | | StopWatch stopWatch = new StopWatch(); |
| | | stopWatch.start(); |
| | | request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch); |
| | | // 打印 Controller 路径 |
| | | printHandlerMethodPosition(handlerMethod); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { |
| | | // 打印 response 日志 |
| | | if (!SpringUtils.isProd()) { |
| | | StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH); |
| | | stopWatch.stop(); |
| | | log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]", |
| | | request.getRequestURI(), stopWatch.getTotalTimeMillis()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 打印 Controller 方法路径 |
| | | */ |
| | | private void printHandlerMethodPosition(HandlerMethod handlerMethod) { |
| | | if (handlerMethod == null) { |
| | | return; |
| | | } |
| | | Method method = handlerMethod.getMethod(); |
| | | Class<?> clazz = method.getDeclaringClass(); |
| | | try { |
| | | // 获取 method 的 lineNumber |
| | | List<String> clazzContents = FileUtil.readUtf8Lines( |
| | | ResourceUtil.getResource(null, clazz).getPath().replace("/target/classes/", "/src/main/java/") |
| | | + clazz.getSimpleName() + ".java"); |
| | | Optional<Integer> lineNumber = IntStream.range(0, clazzContents.size()) |
| | | .filter(i -> clazzContents.get(i).contains(" " + method.getName() + "(")) // 简单匹配,不考虑方法重名 |
| | | .mapToObj(i -> i + 1) // 行号从 1 开始 |
| | | .findFirst(); |
| | | if (!lineNumber.isPresent()) { |
| | | return; |
| | | } |
| | | // 打印结果 |
| | | System.out.printf("\tController 方法路径:%s(%s.java:%d)\n", clazz.getName(), clazz.getSimpleName(), lineNumber.get()); |
| | | } catch (Exception ignore) { |
| | | // 忽略异常。原因:仅仅打印,非重要逻辑 |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.apilog.core.service; |
| | | |
| | | import com.iailab.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; |
| | | |
| | | /** |
| | | * API 访问日志 Framework Service 接口 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface ApiAccessLogFrameworkService { |
| | | |
| | | /** |
| | | * 创建 API 访问日志 |
| | | * |
| | | * @param reqDTO API 访问日志 |
| | | */ |
| | | void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.apilog.core.service; |
| | | |
| | | import com.iailab.module.infra.api.logger.ApiAccessLogApi; |
| | | import com.iailab.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.scheduling.annotation.Async; |
| | | |
| | | /** |
| | | * API 访问日志 Framework Service 实现类 |
| | | * |
| | | * 基于 {@link ApiAccessLogApi} 服务,记录访问日志 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | @Slf4j |
| | | public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService { |
| | | |
| | | private final ApiAccessLogApi apiAccessLogApi; |
| | | |
| | | @Override |
| | | @Async |
| | | public void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO) { |
| | | try { |
| | | apiAccessLogApi.createApiAccessLog(reqDTO); |
| | | } catch (Throwable ex) { |
| | | // 由于 @Async 异步调用,这里打印下日志,更容易跟进 |
| | | log.error("[createApiAccessLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.apilog.core.service; |
| | | |
| | | import com.iailab.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; |
| | | |
| | | /** |
| | | * API 错误日志 Framework Service 接口 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface ApiErrorLogFrameworkService { |
| | | |
| | | /** |
| | | * 创建 API 错误日志 |
| | | * |
| | | * @param reqDTO API 错误日志 |
| | | */ |
| | | void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.apilog.core.service; |
| | | |
| | | import com.iailab.module.infra.api.logger.ApiErrorLogApi; |
| | | import com.iailab.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.scheduling.annotation.Async; |
| | | |
| | | /** |
| | | * API 错误日志 Framework Service 实现类 |
| | | * |
| | | * 基于 {@link ApiErrorLogApi} 服务,记录错误日志 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | @Slf4j |
| | | public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService { |
| | | |
| | | private final ApiErrorLogApi apiErrorLogApi; |
| | | |
| | | @Override |
| | | @Async |
| | | public void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO) { |
| | | try { |
| | | apiErrorLogApi.createApiErrorLog(reqDTO); |
| | | } catch (Throwable ex) { |
| | | // 由于 @Async 异步调用,这里打印下日志,更容易跟进 |
| | | log.error("[createApiErrorLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * API 日志:包含两类 |
| | | * 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。 |
| | | * 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | package com.iailab.framework.apilog; |
对比新文件 |
| | |
| | | package com.iailab.framework.banner.config; |
| | | |
| | | import com.iailab.framework.banner.core.BannerApplicationRunner; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | /** |
| | | * Banner 的自动配置类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | public class IailabBannerAutoConfiguration { |
| | | |
| | | @Bean |
| | | public BannerApplicationRunner bannerApplicationRunner() { |
| | | return new BannerApplicationRunner(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.banner.core; |
| | | |
| | | import cn.hutool.core.thread.ThreadUtil; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.boot.ApplicationArguments; |
| | | import org.springframework.boot.ApplicationRunner; |
| | | |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | | * 项目启动成功后,提供文档相关的地址 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class BannerApplicationRunner implements ApplicationRunner { |
| | | |
| | | @Override |
| | | public void run(ApplicationArguments args) { |
| | | ThreadUtil.execute(() -> { |
| | | ThreadUtil.sleep(1, TimeUnit.SECONDS); // 延迟 1 秒,保证输出到结尾 |
| | | log.info("\n----------------------------------------------------------\n\t" + |
| | | "项目启动成功!\n\t" + |
| | | "----------------------------------------------------------"); |
| | | }); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * Banner 用于在 console 控制台,打印开发文档、接口文档等 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | package com.iailab.framework.banner; |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.base.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler; |
| | | import com.iailab.framework.desensitize.core.base.serializer.StringDesensitizeSerializer; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 顶级脱敏注解,自定义注解需要使用此注解 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target(ElementType.ANNOTATION_TYPE) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside // 此注解是其他所有 jackson 注解的元注解,打上了此注解的注解表明是 jackson 注解的一部分 |
| | | @JsonSerialize(using = StringDesensitizeSerializer.class) // 指定序列化器 |
| | | public @interface DesensitizeBy { |
| | | |
| | | /** |
| | | * 脱敏处理器 |
| | | */ |
| | | @SuppressWarnings("rawtypes") |
| | | Class<? extends DesensitizationHandler> handler(); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.base.handler; |
| | | |
| | | import java.lang.annotation.Annotation; |
| | | |
| | | /** |
| | | * 脱敏处理器接口 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public interface DesensitizationHandler<T extends Annotation> { |
| | | |
| | | /** |
| | | * 脱敏 |
| | | * |
| | | * @param origin 原始字符串 |
| | | * @param annotation 注解信息 |
| | | * @return 脱敏后的字符串 |
| | | */ |
| | | String desensitize(String origin, T annotation); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.base.serializer; |
| | | |
| | | import cn.hutool.core.annotation.AnnotationUtil; |
| | | import cn.hutool.core.lang.Singleton; |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import cn.hutool.core.util.ReflectUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler; |
| | | import com.fasterxml.jackson.core.JsonGenerator; |
| | | import com.fasterxml.jackson.databind.BeanProperty; |
| | | import com.fasterxml.jackson.databind.JsonSerializer; |
| | | import com.fasterxml.jackson.databind.SerializerProvider; |
| | | import com.fasterxml.jackson.databind.ser.ContextualSerializer; |
| | | import com.fasterxml.jackson.databind.ser.std.StdSerializer; |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | |
| | | import java.io.IOException; |
| | | import java.lang.annotation.Annotation; |
| | | import java.lang.reflect.Field; |
| | | |
| | | /** |
| | | * 脱敏序列化器 |
| | | * |
| | | * 实现 JSON 返回数据时,使用 {@link DesensitizationHandler} 对声明脱敏注解的字段,进行脱敏处理。 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @SuppressWarnings("rawtypes") |
| | | public class StringDesensitizeSerializer extends StdSerializer<String> implements ContextualSerializer { |
| | | |
| | | @Getter |
| | | @Setter |
| | | private DesensitizationHandler desensitizationHandler; |
| | | |
| | | protected StringDesensitizeSerializer() { |
| | | super(String.class); |
| | | } |
| | | |
| | | @Override |
| | | public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) { |
| | | DesensitizeBy annotation = beanProperty.getAnnotation(DesensitizeBy.class); |
| | | if (annotation == null) { |
| | | return this; |
| | | } |
| | | // 创建一个 StringDesensitizeSerializer 对象,使用 DesensitizeBy 对应的处理器 |
| | | StringDesensitizeSerializer serializer = new StringDesensitizeSerializer(); |
| | | serializer.setDesensitizationHandler(Singleton.get(annotation.handler())); |
| | | return serializer; |
| | | } |
| | | |
| | | @Override |
| | | @SuppressWarnings("unchecked") |
| | | public void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException { |
| | | if (StrUtil.isBlank(value)) { |
| | | gen.writeNull(); |
| | | return; |
| | | } |
| | | // 获取序列化字段 |
| | | Field field = getField(gen); |
| | | |
| | | // 自定义处理器 |
| | | DesensitizeBy[] annotations = AnnotationUtil.getCombinationAnnotations(field, DesensitizeBy.class); |
| | | if (ArrayUtil.isEmpty(annotations)) { |
| | | gen.writeString(value); |
| | | return; |
| | | } |
| | | for (Annotation annotation : field.getAnnotations()) { |
| | | if (AnnotationUtil.hasAnnotation(annotation.annotationType(), DesensitizeBy.class)) { |
| | | value = this.desensitizationHandler.desensitize(value, annotation); |
| | | gen.writeString(value); |
| | | return; |
| | | } |
| | | } |
| | | gen.writeString(value); |
| | | } |
| | | |
| | | /** |
| | | * 获取字段 |
| | | * |
| | | * @param generator JsonGenerator |
| | | * @return 字段 |
| | | */ |
| | | private Field getField(JsonGenerator generator) { |
| | | String currentName = generator.getOutputContext().getCurrentName(); |
| | | Object currentValue = generator.getCurrentValue(); |
| | | Class<?> currentValueClass = currentValue.getClass(); |
| | | return ReflectUtil.getField(currentValueClass, currentName); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.regex.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.regex.handler.EmailDesensitizationHandler; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 邮箱脱敏注解 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target({ElementType.FIELD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside |
| | | @DesensitizeBy(handler = EmailDesensitizationHandler.class) |
| | | public @interface EmailDesensitize { |
| | | |
| | | /** |
| | | * 匹配的正则表达式 |
| | | */ |
| | | String regex() default "(^.)[^@]*(@.*$)"; |
| | | |
| | | /** |
| | | * 替换规则,邮箱; |
| | | * |
| | | * 比如:example@gmail.com 脱敏之后为 e****@gmail.com |
| | | */ |
| | | String replacer() default "$1****$2"; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.regex.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.regex.handler.DefaultRegexDesensitizationHandler; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 正则脱敏注解 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside |
| | | @DesensitizeBy(handler = DefaultRegexDesensitizationHandler.class) |
| | | public @interface RegexDesensitize { |
| | | |
| | | /** |
| | | * 匹配的正则表达式(默认匹配所有) |
| | | */ |
| | | String regex() default "^[\\s\\S]*$"; |
| | | |
| | | /** |
| | | * 替换规则,会将匹配到的字符串全部替换成 replacer |
| | | * |
| | | * 例如:regex=123; replacer=****** |
| | | * 原始字符串 123456789 |
| | | * 脱敏后字符串 ******456789 |
| | | */ |
| | | String replacer() default "******"; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.regex.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler; |
| | | |
| | | import java.lang.annotation.Annotation; |
| | | |
| | | /** |
| | | * 正则表达式脱敏处理器抽象类,已实现通用的方法 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public abstract class AbstractRegexDesensitizationHandler<T extends Annotation> |
| | | implements DesensitizationHandler<T> { |
| | | |
| | | @Override |
| | | public String desensitize(String origin, T annotation) { |
| | | String regex = getRegex(annotation); |
| | | String replacer = getReplacer(annotation); |
| | | return origin.replaceAll(regex, replacer); |
| | | } |
| | | |
| | | /** |
| | | * 获取注解上的 regex 参数 |
| | | * |
| | | * @param annotation 注解信息 |
| | | * @return 正则表达式 |
| | | */ |
| | | abstract String getRegex(T annotation); |
| | | |
| | | /** |
| | | * 获取注解上的 replacer 参数 |
| | | * |
| | | * @param annotation 注解信息 |
| | | * @return 待替换的字符串 |
| | | */ |
| | | abstract String getReplacer(T annotation); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.regex.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.regex.annotation.RegexDesensitize; |
| | | |
| | | /** |
| | | * {@link RegexDesensitize} 的正则脱敏处理器 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public class DefaultRegexDesensitizationHandler extends AbstractRegexDesensitizationHandler<RegexDesensitize> { |
| | | |
| | | @Override |
| | | String getRegex(RegexDesensitize annotation) { |
| | | return annotation.regex(); |
| | | } |
| | | |
| | | @Override |
| | | String getReplacer(RegexDesensitize annotation) { |
| | | return annotation.replacer(); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.regex.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.regex.annotation.EmailDesensitize; |
| | | |
| | | /** |
| | | * {@link EmailDesensitize} 的脱敏处理器 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public class EmailDesensitizationHandler extends AbstractRegexDesensitizationHandler<EmailDesensitize> { |
| | | |
| | | @Override |
| | | String getRegex(EmailDesensitize annotation) { |
| | | return annotation.regex(); |
| | | } |
| | | |
| | | @Override |
| | | String getReplacer(EmailDesensitize annotation) { |
| | | return annotation.replacer(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.slider.handler.BankCardDesensitization; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 银行卡号 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target({ElementType.FIELD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside |
| | | @DesensitizeBy(handler = BankCardDesensitization.class) |
| | | public @interface BankCardDesensitize { |
| | | |
| | | /** |
| | | * 前缀保留长度 |
| | | */ |
| | | int prefixKeep() default 6; |
| | | |
| | | /** |
| | | * 后缀保留长度 |
| | | */ |
| | | int suffixKeep() default 2; |
| | | |
| | | /** |
| | | * 替换规则,银行卡号; 比如:9988002866797031 脱敏之后为 998800********31 |
| | | */ |
| | | String replacer() default "*"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.slider.handler.CarLicenseDesensitization; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 车牌号 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target({ElementType.FIELD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside |
| | | @DesensitizeBy(handler = CarLicenseDesensitization.class) |
| | | public @interface CarLicenseDesensitize { |
| | | |
| | | /** |
| | | * 前缀保留长度 |
| | | */ |
| | | int prefixKeep() default 3; |
| | | |
| | | /** |
| | | * 后缀保留长度 |
| | | */ |
| | | int suffixKeep() default 1; |
| | | |
| | | /** |
| | | * 替换规则,车牌号;比如:粤A66666 脱敏之后为粤A6***6 |
| | | */ |
| | | String replacer() default "*"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.slider.handler.ChineseNameDesensitization; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 中文名 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target({ElementType.FIELD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside |
| | | @DesensitizeBy(handler = ChineseNameDesensitization.class) |
| | | public @interface ChineseNameDesensitize { |
| | | |
| | | /** |
| | | * 前缀保留长度 |
| | | */ |
| | | int prefixKeep() default 1; |
| | | |
| | | /** |
| | | * 后缀保留长度 |
| | | */ |
| | | int suffixKeep() default 0; |
| | | |
| | | /** |
| | | * 替换规则,中文名;比如:刘子豪脱敏之后为刘** |
| | | */ |
| | | String replacer() default "*"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.slider.handler.FixedPhoneDesensitization; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 固定电话 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target({ElementType.FIELD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside |
| | | @DesensitizeBy(handler = FixedPhoneDesensitization.class) |
| | | public @interface FixedPhoneDesensitize { |
| | | |
| | | /** |
| | | * 前缀保留长度 |
| | | */ |
| | | int prefixKeep() default 4; |
| | | |
| | | /** |
| | | * 后缀保留长度 |
| | | */ |
| | | int suffixKeep() default 2; |
| | | |
| | | /** |
| | | * 替换规则,固定电话;比如:01086551122 脱敏之后为 0108*****22 |
| | | */ |
| | | String replacer() default "*"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.slider.handler.IdCardDesensitization; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 身份证 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target({ElementType.FIELD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside |
| | | @DesensitizeBy(handler = IdCardDesensitization.class) |
| | | public @interface IdCardDesensitize { |
| | | |
| | | /** |
| | | * 前缀保留长度 |
| | | */ |
| | | int prefixKeep() default 6; |
| | | |
| | | /** |
| | | * 后缀保留长度 |
| | | */ |
| | | int suffixKeep() default 2; |
| | | |
| | | /** |
| | | * 替换规则,身份证号码;比如:530321199204074611 脱敏之后为 530321**********11 |
| | | */ |
| | | String replacer() default "*"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.slider.handler.MobileDesensitization; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 手机号 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target({ElementType.FIELD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside |
| | | @DesensitizeBy(handler = MobileDesensitization.class) |
| | | public @interface MobileDesensitize { |
| | | |
| | | /** |
| | | * 前缀保留长度 |
| | | */ |
| | | int prefixKeep() default 3; |
| | | |
| | | /** |
| | | * 后缀保留长度 |
| | | */ |
| | | int suffixKeep() default 4; |
| | | |
| | | /** |
| | | * 替换规则,手机号;比如:13248765917 脱敏之后为 132****5917 |
| | | */ |
| | | String replacer() default "*"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.slider.handler.PasswordDesensitization; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 密码 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target({ElementType.FIELD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside |
| | | @DesensitizeBy(handler = PasswordDesensitization.class) |
| | | public @interface PasswordDesensitize { |
| | | |
| | | /** |
| | | * 前缀保留长度 |
| | | */ |
| | | int prefixKeep() default 0; |
| | | |
| | | /** |
| | | * 后缀保留长度 |
| | | */ |
| | | int suffixKeep() default 0; |
| | | |
| | | /** |
| | | * 替换规则,密码; |
| | | * |
| | | * 比如:123456 脱敏之后为 ****** |
| | | */ |
| | | String replacer() default "*"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.slider.handler.DefaultDesensitizationHandler; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 滑动脱敏注解 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside |
| | | @DesensitizeBy(handler = DefaultDesensitizationHandler.class) |
| | | public @interface SliderDesensitize { |
| | | |
| | | /** |
| | | * 后缀保留长度 |
| | | */ |
| | | int suffixKeep() default 0; |
| | | |
| | | /** |
| | | * 替换规则,会将前缀后缀保留后,全部替换成 replacer |
| | | * |
| | | * 例如:prefixKeep = 1; suffixKeep = 2; replacer = "*"; |
| | | * 原始字符串 123456 |
| | | * 脱敏后 1***56 |
| | | */ |
| | | String replacer() default "*"; |
| | | |
| | | /** |
| | | * 前缀保留长度 |
| | | */ |
| | | int prefixKeep() default 0; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler; |
| | | |
| | | import java.lang.annotation.Annotation; |
| | | |
| | | /** |
| | | * 滑动脱敏处理器抽象类,已实现通用的方法 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public abstract class AbstractSliderDesensitizationHandler<T extends Annotation> |
| | | implements DesensitizationHandler<T> { |
| | | |
| | | @Override |
| | | public String desensitize(String origin, T annotation) { |
| | | int prefixKeep = getPrefixKeep(annotation); |
| | | int suffixKeep = getSuffixKeep(annotation); |
| | | String replacer = getReplacer(annotation); |
| | | int length = origin.length(); |
| | | |
| | | // 情况一:原始字符串长度小于等于保留长度,则原始字符串全部替换 |
| | | if (prefixKeep >= length || suffixKeep >= length) { |
| | | return buildReplacerByLength(replacer, length); |
| | | } |
| | | |
| | | // 情况二:原始字符串长度小于等于前后缀保留字符串长度,则原始字符串全部替换 |
| | | if ((prefixKeep + suffixKeep) >= length) { |
| | | return buildReplacerByLength(replacer, length); |
| | | } |
| | | |
| | | // 情况三:原始字符串长度大于前后缀保留字符串长度,则替换中间字符串 |
| | | int interval = length - prefixKeep - suffixKeep; |
| | | return origin.substring(0, prefixKeep) + |
| | | buildReplacerByLength(replacer, interval) + |
| | | origin.substring(prefixKeep + interval); |
| | | } |
| | | |
| | | /** |
| | | * 根据长度循环构建替换符 |
| | | * |
| | | * @param replacer 替换符 |
| | | * @param length 长度 |
| | | * @return 构建后的替换符 |
| | | */ |
| | | private String buildReplacerByLength(String replacer, int length) { |
| | | StringBuilder builder = new StringBuilder(); |
| | | for (int i = 0; i < length; i++) { |
| | | builder.append(replacer); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | /** |
| | | * 前缀保留长度 |
| | | * |
| | | * @param annotation 注解信息 |
| | | * @return 前缀保留长度 |
| | | */ |
| | | abstract Integer getPrefixKeep(T annotation); |
| | | |
| | | /** |
| | | * 后缀保留长度 |
| | | * |
| | | * @param annotation 注解信息 |
| | | * @return 后缀保留长度 |
| | | */ |
| | | abstract Integer getSuffixKeep(T annotation); |
| | | |
| | | /** |
| | | * 替换符 |
| | | * |
| | | * @param annotation 注解信息 |
| | | * @return 替换符 |
| | | */ |
| | | abstract String getReplacer(T annotation); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.slider.annotation.BankCardDesensitize; |
| | | |
| | | /** |
| | | * {@link BankCardDesensitize} 的脱敏处理器 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public class BankCardDesensitization extends AbstractSliderDesensitizationHandler<BankCardDesensitize> { |
| | | |
| | | @Override |
| | | Integer getPrefixKeep(BankCardDesensitize annotation) { |
| | | return annotation.prefixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | Integer getSuffixKeep(BankCardDesensitize annotation) { |
| | | return annotation.suffixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | String getReplacer(BankCardDesensitize annotation) { |
| | | return annotation.replacer(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.slider.annotation.CarLicenseDesensitize; |
| | | |
| | | /** |
| | | * {@link CarLicenseDesensitize} 的脱敏处理器 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public class CarLicenseDesensitization extends AbstractSliderDesensitizationHandler<CarLicenseDesensitize> { |
| | | @Override |
| | | Integer getPrefixKeep(CarLicenseDesensitize annotation) { |
| | | return annotation.prefixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | Integer getSuffixKeep(CarLicenseDesensitize annotation) { |
| | | return annotation.suffixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | String getReplacer(CarLicenseDesensitize annotation) { |
| | | return annotation.replacer(); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.slider.annotation.ChineseNameDesensitize; |
| | | |
| | | /** |
| | | * {@link ChineseNameDesensitize} 的脱敏处理器 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public class ChineseNameDesensitization extends AbstractSliderDesensitizationHandler<ChineseNameDesensitize> { |
| | | |
| | | @Override |
| | | Integer getPrefixKeep(ChineseNameDesensitize annotation) { |
| | | return annotation.prefixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | Integer getSuffixKeep(ChineseNameDesensitize annotation) { |
| | | return annotation.suffixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | String getReplacer(ChineseNameDesensitize annotation) { |
| | | return annotation.replacer(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.slider.annotation.SliderDesensitize; |
| | | |
| | | /** |
| | | * {@link SliderDesensitize} 的脱敏处理器 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public class DefaultDesensitizationHandler extends AbstractSliderDesensitizationHandler<SliderDesensitize> { |
| | | @Override |
| | | Integer getPrefixKeep(SliderDesensitize annotation) { |
| | | return annotation.prefixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | Integer getSuffixKeep(SliderDesensitize annotation) { |
| | | return annotation.suffixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | String getReplacer(SliderDesensitize annotation) { |
| | | return annotation.replacer(); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize; |
| | | |
| | | /** |
| | | * {@link FixedPhoneDesensitize} 的脱敏处理器 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public class FixedPhoneDesensitization extends AbstractSliderDesensitizationHandler<FixedPhoneDesensitize> { |
| | | @Override |
| | | Integer getPrefixKeep(FixedPhoneDesensitize annotation) { |
| | | return annotation.prefixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | Integer getSuffixKeep(FixedPhoneDesensitize annotation) { |
| | | return annotation.suffixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | String getReplacer(FixedPhoneDesensitize annotation) { |
| | | return annotation.replacer(); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.slider.annotation.IdCardDesensitize; |
| | | |
| | | /** |
| | | * {@link IdCardDesensitize} 的脱敏处理器 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public class IdCardDesensitization extends AbstractSliderDesensitizationHandler<IdCardDesensitize> { |
| | | @Override |
| | | Integer getPrefixKeep(IdCardDesensitize annotation) { |
| | | return annotation.prefixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | Integer getSuffixKeep(IdCardDesensitize annotation) { |
| | | return annotation.suffixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | String getReplacer(IdCardDesensitize annotation) { |
| | | return annotation.replacer(); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.slider.annotation.MobileDesensitize; |
| | | |
| | | /** |
| | | * {@link MobileDesensitize} 的脱敏处理器 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public class MobileDesensitization extends AbstractSliderDesensitizationHandler<MobileDesensitize> { |
| | | |
| | | @Override |
| | | Integer getPrefixKeep(MobileDesensitize annotation) { |
| | | return annotation.prefixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | Integer getSuffixKeep(MobileDesensitize annotation) { |
| | | return annotation.suffixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | String getReplacer(MobileDesensitize annotation) { |
| | | return annotation.replacer(); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.slider.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.slider.annotation.PasswordDesensitize; |
| | | |
| | | /** |
| | | * {@link PasswordDesensitize} 的码脱敏处理器 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | public class PasswordDesensitization extends AbstractSliderDesensitizationHandler<PasswordDesensitize> { |
| | | @Override |
| | | Integer getPrefixKeep(PasswordDesensitize annotation) { |
| | | return annotation.prefixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | Integer getSuffixKeep(PasswordDesensitize annotation) { |
| | | return annotation.suffixKeep(); |
| | | } |
| | | |
| | | @Override |
| | | String getReplacer(PasswordDesensitize annotation) { |
| | | return annotation.replacer(); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 脱敏组件:支持 JSON 返回数据时,将邮箱、手机等字段进行脱敏 |
| | | */ |
| | | package com.iailab.framework.desensitize; |
对比新文件 |
| | |
| | | package com.iailab.framework.jackson.config; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.fasterxml.jackson.databind.module.SimpleModule; |
| | | import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; |
| | | import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; |
| | | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; |
| | | import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; |
| | | import com.iailab.framework.common.util.json.databind.NumberSerializer; |
| | | import com.iailab.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer; |
| | | import com.iailab.framework.common.util.json.databind.TimestampLocalDateTimeSerializer; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.LocalTime; |
| | | import java.util.List; |
| | | |
| | | @AutoConfiguration |
| | | @Slf4j |
| | | public class IailabJacksonAutoConfiguration { |
| | | |
| | | @Bean |
| | | @SuppressWarnings("InstantiationOfUtilityClass") |
| | | public JsonUtils jsonUtils(List<ObjectMapper> objectMappers) { |
| | | // 1.1 创建 SimpleModule 对象 |
| | | SimpleModule simpleModule = new SimpleModule(); |
| | | simpleModule |
| | | // 新增 Long 类型序列化规则,数值超过 2^53-1,在 JS 会出现精度丢失问题,因此 Long 自动序列化为字符串类型 |
| | | .addSerializer(Long.class, NumberSerializer.INSTANCE) |
| | | .addSerializer(Long.TYPE, NumberSerializer.INSTANCE) |
| | | .addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE) |
| | | .addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE) |
| | | .addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE) |
| | | .addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE) |
| | | // 新增 LocalDateTime 序列化、反序列化规则,使用 Long 时间戳 |
| | | .addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE) |
| | | .addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); |
| | | // 1.2 注册到 objectMapper |
| | | objectMappers.forEach(objectMapper -> objectMapper.registerModule(simpleModule)); |
| | | |
| | | // 2. 设置 objectMapper 到 JsonUtils |
| | | JsonUtils.init(CollUtil.getFirst(objectMappers)); |
| | | log.info("[init][初始化 JsonUtils 成功]"); |
| | | return new JsonUtils(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.jackson.core.databind; |
| | | |
| | | import com.fasterxml.jackson.core.JsonGenerator; |
| | | import com.fasterxml.jackson.databind.SerializerProvider; |
| | | import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; |
| | | |
| | | import java.io.IOException; |
| | | |
| | | /** |
| | | * Long 序列化规则 |
| | | * |
| | | * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题 |
| | | * |
| | | * @author 星语 |
| | | */ |
| | | @JacksonStdImpl |
| | | public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer { |
| | | |
| | | private static final long MAX_SAFE_INTEGER = 9007199254740991L; |
| | | private static final long MIN_SAFE_INTEGER = -9007199254740991L; |
| | | |
| | | public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class); |
| | | |
| | | public NumberSerializer(Class<? extends Number> rawType) { |
| | | super(rawType); |
| | | } |
| | | |
| | | @Override |
| | | public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException { |
| | | // 超出范围 序列化位字符串 |
| | | if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) { |
| | | super.serialize(value, gen, serializers); |
| | | } else { |
| | | gen.writeString(value.toString()); |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.jackson.core.databind; |
| | | |
| | | import com.fasterxml.jackson.core.JsonParser; |
| | | import com.fasterxml.jackson.databind.DeserializationContext; |
| | | import com.fasterxml.jackson.databind.JsonDeserializer; |
| | | |
| | | import java.io.IOException; |
| | | import java.time.Instant; |
| | | import java.time.LocalDateTime; |
| | | import java.time.ZoneId; |
| | | |
| | | /** |
| | | * 基于时间戳的 LocalDateTime 反序列化器 |
| | | * |
| | | * @author 老五 |
| | | */ |
| | | public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> { |
| | | |
| | | public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer(); |
| | | |
| | | @Override |
| | | public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { |
| | | // 将 Long 时间戳,转换为 LocalDateTime 对象 |
| | | return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.jackson.core.databind; |
| | | |
| | | import com.fasterxml.jackson.core.JsonGenerator; |
| | | import com.fasterxml.jackson.databind.JsonSerializer; |
| | | import com.fasterxml.jackson.databind.SerializerProvider; |
| | | |
| | | import java.io.IOException; |
| | | import java.time.LocalDateTime; |
| | | import java.time.ZoneId; |
| | | |
| | | /** |
| | | * 基于时间戳的 LocalDateTime 序列化器 |
| | | * |
| | | * @author 老五 |
| | | */ |
| | | public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> { |
| | | |
| | | public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer(); |
| | | |
| | | @Override |
| | | public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { |
| | | // 将 LocalDateTime 对象,转换为 Long 时间戳 |
| | | gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.jackson.core; |
对比新文件 |
| | |
| | | /** |
| | | * Web 框架,全局异常、API 日志等 |
| | | */ |
| | | package com.iailab.framework; |
对比新文件 |
| | |
| | | package com.iailab.framework.swagger.config; |
| | | |
| | | import io.swagger.v3.oas.models.Components; |
| | | import io.swagger.v3.oas.models.OpenAPI; |
| | | import io.swagger.v3.oas.models.info.Contact; |
| | | import io.swagger.v3.oas.models.info.Info; |
| | | import io.swagger.v3.oas.models.info.License; |
| | | import io.swagger.v3.oas.models.media.IntegerSchema; |
| | | import io.swagger.v3.oas.models.media.StringSchema; |
| | | import io.swagger.v3.oas.models.parameters.Parameter; |
| | | import io.swagger.v3.oas.models.security.SecurityRequirement; |
| | | import io.swagger.v3.oas.models.security.SecurityScheme; |
| | | import org.springdoc.core.*; |
| | | import org.springdoc.core.customizers.OpenApiBuilderCustomizer; |
| | | import org.springdoc.core.customizers.ServerBaseUrlCustomizer; |
| | | import org.springdoc.core.providers.JavadocProvider; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Primary; |
| | | import org.springframework.http.HttpHeaders; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Optional; |
| | | |
| | | import static com.iailab.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
| | | |
| | | /** |
| | | * Swagger 自动配置类,基于 OpenAPI + Springdoc 实现。 |
| | | * |
| | | * 友情提示: |
| | | * 1. Springdoc 文档地址:<a href="https://github.com/springdoc/springdoc-openapi">仓库</a> |
| | | * 2. Swagger 规范,于 2015 更名为 OpenAPI 规范,本质是一个东西 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @ConditionalOnClass({OpenAPI.class}) |
| | | @EnableConfigurationProperties(SwaggerProperties.class) |
| | | @ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用 |
| | | public class IailabSwaggerAutoConfiguration { |
| | | |
| | | // ========== 全局 OpenAPI 配置 ========== |
| | | |
| | | @Bean |
| | | public OpenAPI createApi(SwaggerProperties properties) { |
| | | Map<String, SecurityScheme> securitySchemas = buildSecuritySchemes(); |
| | | OpenAPI openAPI = new OpenAPI() |
| | | // 接口信息 |
| | | .info(buildInfo(properties)) |
| | | // 接口安全配置 |
| | | .components(new Components().securitySchemes(securitySchemas)) |
| | | .addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)); |
| | | securitySchemas.keySet().forEach(key -> openAPI.addSecurityItem(new SecurityRequirement().addList(key))); |
| | | return openAPI; |
| | | } |
| | | |
| | | /** |
| | | * API 摘要信息 |
| | | */ |
| | | private Info buildInfo(SwaggerProperties properties) { |
| | | return new Info() |
| | | .title(properties.getTitle()) |
| | | .description(properties.getDescription()) |
| | | .version(properties.getVersion()) |
| | | .contact(new Contact().name(properties.getAuthor()).url(properties.getUrl()).email(properties.getEmail())) |
| | | .license(new License().name(properties.getLicense()).url(properties.getLicenseUrl())); |
| | | } |
| | | |
| | | /** |
| | | * 安全模式,这里配置通过请求头 Authorization 传递 token 参数 |
| | | */ |
| | | private Map<String, SecurityScheme> buildSecuritySchemes() { |
| | | Map<String, SecurityScheme> securitySchemes = new HashMap<>(); |
| | | SecurityScheme securityScheme = new SecurityScheme() |
| | | .type(SecurityScheme.Type.APIKEY) // 类型 |
| | | .name(HttpHeaders.AUTHORIZATION) // 请求头的 name |
| | | .in(SecurityScheme.In.HEADER); // token 所在位置 |
| | | securitySchemes.put(HttpHeaders.AUTHORIZATION, securityScheme); |
| | | return securitySchemes; |
| | | } |
| | | |
| | | /** |
| | | * 自定义 OpenAPI 处理器 |
| | | */ |
| | | @Bean |
| | | @Primary // 目的:以我们创建的 OpenAPIService Bean 为主,避免一键改包后,启动报错! |
| | | public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI, |
| | | SecurityService securityParser, |
| | | SpringDocConfigProperties springDocConfigProperties, |
| | | PropertyResolverUtils propertyResolverUtils, |
| | | Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers, |
| | | Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers, |
| | | Optional<JavadocProvider> javadocProvider) { |
| | | |
| | | return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, |
| | | propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider); |
| | | } |
| | | |
| | | // ========== 分组 OpenAPI 配置 ========== |
| | | |
| | | /** |
| | | * 所有模块的 API 分组 |
| | | */ |
| | | @Bean |
| | | public GroupedOpenApi allGroupedOpenApi() { |
| | | return buildGroupedOpenApi("all", ""); |
| | | } |
| | | |
| | | public static GroupedOpenApi buildGroupedOpenApi(String group) { |
| | | return buildGroupedOpenApi(group, group); |
| | | } |
| | | |
| | | public static GroupedOpenApi buildGroupedOpenApi(String group, String path) { |
| | | return GroupedOpenApi.builder() |
| | | .group(group) |
| | | .pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**") |
| | | .addOperationCustomizer((operation, handlerMethod) -> operation |
| | | .addParametersItem(buildTenantHeaderParameter()) |
| | | .addParametersItem(buildSecurityHeaderParameter())) |
| | | .build(); |
| | | } |
| | | |
| | | /** |
| | | * 构建 Tenant 租户编号请求头参数 |
| | | * |
| | | * @return 多租户参数 |
| | | */ |
| | | private static Parameter buildTenantHeaderParameter() { |
| | | return new Parameter() |
| | | .name(HEADER_TENANT_ID) // header 名 |
| | | .description("租户编号") // 描述 |
| | | .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header |
| | | .schema(new IntegerSchema()._default(1L).name(HEADER_TENANT_ID).description("租户编号")); // 默认:使用租户编号为 1 |
| | | } |
| | | |
| | | /** |
| | | * 构建 Authorization 认证请求头参数 |
| | | * |
| | | * 解决 Knife4j <a href="https://gitee.com/xiaoym/knife4j/issues/I69QBU">Authorize 未生效,请求header里未包含参数</a> |
| | | * |
| | | * @return 认证参数 |
| | | */ |
| | | private static Parameter buildSecurityHeaderParameter() { |
| | | return new Parameter() |
| | | .name(HttpHeaders.AUTHORIZATION) // header 名 |
| | | .description("认证 Token") // 描述 |
| | | .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header |
| | | .schema(new StringSchema()._default("Bearer test1").name(HEADER_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1 |
| | | } |
| | | |
| | | } |
| | | |
对比新文件 |
| | |
| | | package com.iailab.framework.swagger.config; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | |
| | | import javax.validation.constraints.NotEmpty; |
| | | |
| | | /** |
| | | * Swagger 配置属性 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @ConfigurationProperties("iailab.swagger") |
| | | @Data |
| | | public class SwaggerProperties { |
| | | |
| | | /** |
| | | * 标题 |
| | | */ |
| | | @NotEmpty(message = "标题不能为空") |
| | | private String title; |
| | | /** |
| | | * 描述 |
| | | */ |
| | | @NotEmpty(message = "描述不能为空") |
| | | private String description; |
| | | /** |
| | | * 作者 |
| | | */ |
| | | @NotEmpty(message = "作者不能为空") |
| | | private String author; |
| | | /** |
| | | * 版本 |
| | | */ |
| | | @NotEmpty(message = "版本不能为空") |
| | | private String version; |
| | | /** |
| | | * url |
| | | */ |
| | | @NotEmpty(message = "扫描的 package 不能为空") |
| | | private String url; |
| | | /** |
| | | * email |
| | | */ |
| | | @NotEmpty(message = "扫描的 email 不能为空") |
| | | private String email; |
| | | |
| | | /** |
| | | * license |
| | | */ |
| | | @NotEmpty(message = "扫描的 license 不能为空") |
| | | private String license; |
| | | |
| | | /** |
| | | * license-url |
| | | */ |
| | | @NotEmpty(message = "扫描的 license-url 不能为空") |
| | | private String licenseUrl; |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 基于 Swagger + Knife4j 实现 API 接口文档 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | package com.iailab.framework.swagger; |
对比新文件 |
| | |
| | | package com.iailab.framework.web.config; |
| | | |
| | | import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkService; |
| | | import com.iailab.framework.common.enums.WebFilterOrderEnum; |
| | | import com.iailab.framework.web.core.filter.CacheRequestBodyFilter; |
| | | import com.iailab.framework.web.core.filter.DemoFilter; |
| | | import com.iailab.framework.web.core.handler.GlobalExceptionHandler; |
| | | import com.iailab.framework.web.core.handler.GlobalResponseBodyHandler; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.boot.web.client.RestTemplateBuilder; |
| | | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.util.AntPathMatcher; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.client.RestTemplate; |
| | | 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.PathMatchConfigurer; |
| | | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.servlet.Filter; |
| | | |
| | | @AutoConfiguration |
| | | @EnableConfigurationProperties(WebProperties.class) |
| | | public class IailabWebAutoConfiguration implements WebMvcConfigurer { |
| | | |
| | | @Resource |
| | | private WebProperties webProperties; |
| | | /** |
| | | * 应用名 |
| | | */ |
| | | @Value("${spring.application.name}") |
| | | private String applicationName; |
| | | |
| | | @Override |
| | | public void configurePathMatch(PathMatchConfigurer configurer) { |
| | | configurePathMatch(configurer, webProperties.getAdminApi()); |
| | | configurePathMatch(configurer, webProperties.getAppApi()); |
| | | } |
| | | |
| | | /** |
| | | * 设置 API 前缀,仅仅匹配 controller 包下的 |
| | | * |
| | | * @param configurer 配置 |
| | | * @param api API 配置 |
| | | */ |
| | | private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) { |
| | | AntPathMatcher antPathMatcher = new AntPathMatcher("."); |
| | | configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class) |
| | | && antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包 |
| | | } |
| | | |
| | | @Bean |
| | | public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) { |
| | | return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService); |
| | | } |
| | | |
| | | @Bean |
| | | public GlobalResponseBodyHandler globalResponseBodyHandler() { |
| | | return new GlobalResponseBodyHandler(); |
| | | } |
| | | |
| | | @Bean |
| | | @SuppressWarnings("InstantiationOfUtilityClass") |
| | | public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) { |
| | | // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean |
| | | return new WebFrameworkUtils(webProperties); |
| | | } |
| | | |
| | | // ========== Filter 相关 ========== |
| | | |
| | | /** |
| | | * 创建 CorsFilter Bean,解决跨域问题 |
| | | */ |
| | | @Bean |
| | | public FilterRegistrationBean<CorsFilter> corsFilterBean() { |
| | | // 创建 CorsConfiguration 对象 |
| | | CorsConfiguration config = new CorsConfiguration(); |
| | | config.setAllowCredentials(true); |
| | | config.addAllowedOriginPattern("*"); // 设置访问源地址 |
| | | config.addAllowedHeader("*"); // 设置访问源请求头 |
| | | config.addAllowedMethod("*"); // 设置访问源请求方法 |
| | | // 创建 UrlBasedCorsConfigurationSource 对象 |
| | | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); |
| | | source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 |
| | | return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER); |
| | | } |
| | | |
| | | /** |
| | | * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容 |
| | | */ |
| | | @Bean |
| | | public FilterRegistrationBean<CacheRequestBodyFilter> requestBodyCacheFilter() { |
| | | return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER); |
| | | } |
| | | |
| | | /** |
| | | * 创建 DemoFilter Bean,演示模式 |
| | | */ |
| | | @Bean |
| | | @ConditionalOnProperty(value = "iailab.demo", havingValue = "true") |
| | | public FilterRegistrationBean<DemoFilter> demoFilter() { |
| | | return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER); |
| | | } |
| | | |
| | | public static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) { |
| | | FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter); |
| | | bean.setOrder(order); |
| | | return bean; |
| | | } |
| | | |
| | | /** |
| | | * 创建 RestTemplate 实例 |
| | | * |
| | | * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder} |
| | | */ |
| | | @Bean |
| | | @ConditionalOnMissingBean |
| | | public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { |
| | | return restTemplateBuilder.build(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.web.config; |
| | | |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Data; |
| | | import lombok.NoArgsConstructor; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; |
| | | |
| | | import javax.validation.Valid; |
| | | import javax.validation.constraints.NotEmpty; |
| | | import javax.validation.constraints.NotNull; |
| | | |
| | | @ConfigurationProperties(prefix = "iailab.web") |
| | | @Validated |
| | | @Data |
| | | public class WebProperties { |
| | | |
| | | @NotNull(message = "APP API 不能为空") |
| | | private Api appApi = new Api("/app-api", "**.controller.app.**"); |
| | | @NotNull(message = "Admin API 不能为空") |
| | | private Api adminApi = new Api("/admin-api", "**.controller.admin.**"); |
| | | |
| | | @NotNull(message = "Admin UI 不能为空") |
| | | private Ui adminUi; |
| | | |
| | | @Data |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Valid |
| | | public static class Api { |
| | | |
| | | /** |
| | | * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀 |
| | | * |
| | | * |
| | | * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题 |
| | | * 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。 |
| | | * |
| | | * @see IailabWebAutoConfiguration#configurePathMatch(PathMatchConfigurer) |
| | | */ |
| | | @NotEmpty(message = "API 前缀不能为空") |
| | | private String prefix; |
| | | |
| | | /** |
| | | * Controller 所在包的 Ant 路径规则 |
| | | * |
| | | * 主要目的是,给该 Controller 设置指定的 {@link #prefix} |
| | | */ |
| | | @NotEmpty(message = "Controller 所在包不能为空") |
| | | private String controller; |
| | | |
| | | } |
| | | |
| | | @Data |
| | | @Valid |
| | | public static class Ui { |
| | | |
| | | /** |
| | | * 访问地址 |
| | | */ |
| | | private String url; |
| | | |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.web.core.filter; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.web.config.WebProperties; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.web.filter.OncePerRequestFilter; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | |
| | | /** |
| | | * 过滤 /admin-api、/app-api 等 API 请求的过滤器 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | public abstract class ApiRequestFilter extends OncePerRequestFilter { |
| | | |
| | | protected final WebProperties webProperties; |
| | | |
| | | @Override |
| | | protected boolean shouldNotFilter(HttpServletRequest request) { |
| | | // 只过滤 API 请求的地址 |
| | | return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(), |
| | | webProperties.getAppApi().getPrefix()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.web.core.filter; |
| | | |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import org.springframework.web.filter.OncePerRequestFilter; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | |
| | | /** |
| | | * Request Body 缓存 Filter,实现它的可重复读取 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class CacheRequestBodyFilter extends OncePerRequestFilter { |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
| | | throws IOException, ServletException { |
| | | filterChain.doFilter(new CacheRequestBodyWrapper(request), response); |
| | | } |
| | | |
| | | @Override |
| | | protected boolean shouldNotFilter(HttpServletRequest request) { |
| | | // 只处理 json 请求内容 |
| | | return !ServletUtils.isJsonRequest(request); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.web.core.filter; |
| | | |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | |
| | | import javax.servlet.ReadListener; |
| | | import javax.servlet.ServletInputStream; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletRequestWrapper; |
| | | import java.io.BufferedReader; |
| | | import java.io.ByteArrayInputStream; |
| | | import java.io.IOException; |
| | | import java.io.InputStreamReader; |
| | | |
| | | /** |
| | | * Request Body 缓存 Wrapper |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { |
| | | |
| | | /** |
| | | * 缓存的内容 |
| | | */ |
| | | private final byte[] body; |
| | | |
| | | public CacheRequestBodyWrapper(HttpServletRequest request) { |
| | | super(request); |
| | | body = ServletUtils.getBodyBytes(request); |
| | | } |
| | | |
| | | @Override |
| | | public BufferedReader getReader() throws IOException { |
| | | return new BufferedReader(new InputStreamReader(this.getInputStream())); |
| | | } |
| | | |
| | | @Override |
| | | public ServletInputStream getInputStream() throws IOException { |
| | | final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); |
| | | // 返回 ServletInputStream |
| | | return new ServletInputStream() { |
| | | |
| | | @Override |
| | | public int read() { |
| | | return inputStream.read(); |
| | | } |
| | | |
| | | @Override |
| | | public boolean isFinished() { |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public boolean isReady() { |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public void setReadListener(ReadListener readListener) {} |
| | | |
| | | @Override |
| | | public int available() { |
| | | return body.length; |
| | | } |
| | | |
| | | }; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.web.core.filter; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.pojo.CommonResult; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import org.springframework.web.filter.OncePerRequestFilter; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | |
| | | import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY; |
| | | |
| | | /** |
| | | * 演示 Filter,禁止用户发起写操作,避免影响测试数据 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DemoFilter extends OncePerRequestFilter { |
| | | |
| | | @Override |
| | | protected boolean shouldNotFilter(HttpServletRequest request) { |
| | | String method = request.getMethod(); |
| | | return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE") // 写操作时,不进行过滤率 |
| | | || WebFrameworkUtils.getLoginUserId(request) == null; // 非登录用户时,不进行过滤 |
| | | } |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { |
| | | // 直接返回 DEMO_DENY 的结果。即,请求不继续 |
| | | ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.web.core.handler; |
| | | |
| | | import cn.hutool.core.exceptions.ExceptionUtil; |
| | | import cn.hutool.core.map.MapUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkService; |
| | | import com.iailab.framework.common.exception.ServiceException; |
| | | import com.iailab.framework.common.pojo.CommonResult; |
| | | import com.iailab.framework.common.util.collection.SetUtils; |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.common.util.monitor.TracerUtils; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import com.iailab.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.security.access.AccessDeniedException; |
| | | import org.springframework.util.Assert; |
| | | import org.springframework.validation.BindException; |
| | | import org.springframework.validation.FieldError; |
| | | import org.springframework.web.HttpRequestMethodNotSupportedException; |
| | | import org.springframework.web.bind.MethodArgumentNotValidException; |
| | | import org.springframework.web.bind.MissingServletRequestParameterException; |
| | | import org.springframework.web.bind.annotation.ExceptionHandler; |
| | | import org.springframework.web.bind.annotation.RestControllerAdvice; |
| | | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; |
| | | import org.springframework.web.servlet.NoHandlerFoundException; |
| | | import org.springframework.dao.DuplicateKeyException; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.validation.ConstraintViolation; |
| | | import javax.validation.ConstraintViolationException; |
| | | import javax.validation.ValidationException; |
| | | import java.time.LocalDateTime; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.*; |
| | | |
| | | /** |
| | | * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RestControllerAdvice |
| | | @AllArgsConstructor |
| | | @Slf4j |
| | | public class GlobalExceptionHandler { |
| | | |
| | | /** |
| | | * 忽略的 ServiceException 错误提示,避免打印过多 logger |
| | | */ |
| | | public static final Set<String> IGNORE_ERROR_MESSAGES = SetUtils.asSet("无效的刷新令牌"); |
| | | |
| | | @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") |
| | | private final String applicationName; |
| | | |
| | | private final ApiErrorLogFrameworkService apiErrorLogFrameworkService; |
| | | |
| | | /** |
| | | * 处理所有异常,主要是提供给 Filter 使用 |
| | | * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。 |
| | | * |
| | | * @param request 请求 |
| | | * @param ex 异常 |
| | | * @return 通用返回 |
| | | */ |
| | | public CommonResult<?> allExceptionHandler(HttpServletRequest request, Throwable ex) { |
| | | if (ex instanceof MissingServletRequestParameterException) { |
| | | return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex); |
| | | } |
| | | if (ex instanceof MethodArgumentTypeMismatchException) { |
| | | return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex); |
| | | } |
| | | if (ex instanceof MethodArgumentNotValidException) { |
| | | return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex); |
| | | } |
| | | if (ex instanceof BindException) { |
| | | return bindExceptionHandler((BindException) ex); |
| | | } |
| | | if (ex instanceof ConstraintViolationException) { |
| | | return constraintViolationExceptionHandler((ConstraintViolationException) ex); |
| | | } |
| | | if (ex instanceof ValidationException) { |
| | | return validationException((ValidationException) ex); |
| | | } |
| | | if (ex instanceof NoHandlerFoundException) { |
| | | return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex); |
| | | } |
| | | if (ex instanceof HttpRequestMethodNotSupportedException) { |
| | | return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); |
| | | } |
| | | if (ex instanceof ServiceException) { |
| | | return serviceExceptionHandler((ServiceException) ex); |
| | | } |
| | | if (ex instanceof AccessDeniedException) { |
| | | return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); |
| | | } |
| | | if (ex instanceof DuplicateKeyException) { |
| | | return duplicateKeyExceptionHandler((DuplicateKeyException) ex); |
| | | } |
| | | return defaultExceptionHandler(request, ex); |
| | | } |
| | | |
| | | /** |
| | | * 处理 SpringMVC 请求参数缺失 |
| | | * |
| | | * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数 |
| | | */ |
| | | @ExceptionHandler(value = MissingServletRequestParameterException.class) |
| | | public CommonResult<?> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) { |
| | | log.warn("[missingServletRequestParameterExceptionHandler]", ex); |
| | | return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName())); |
| | | } |
| | | |
| | | /** |
| | | * 处理 SpringMVC 请求参数类型错误 |
| | | * |
| | | * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String |
| | | */ |
| | | @ExceptionHandler(MethodArgumentTypeMismatchException.class) |
| | | public CommonResult<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { |
| | | log.warn("[missingServletRequestParameterExceptionHandler]", ex); |
| | | return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); |
| | | } |
| | | |
| | | /** |
| | | * 处理 SpringMVC 参数校验不正确 |
| | | */ |
| | | @ExceptionHandler(MethodArgumentNotValidException.class) |
| | | public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { |
| | | log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); |
| | | FieldError fieldError = ex.getBindingResult().getFieldError(); |
| | | assert fieldError != null; // 断言,避免告警 |
| | | return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); |
| | | } |
| | | |
| | | /** |
| | | * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验 |
| | | */ |
| | | @ExceptionHandler(BindException.class) |
| | | public CommonResult<?> bindExceptionHandler(BindException ex) { |
| | | log.warn("[handleBindException]", ex); |
| | | FieldError fieldError = ex.getFieldError(); |
| | | assert fieldError != null; // 断言,避免告警 |
| | | return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); |
| | | } |
| | | |
| | | /** |
| | | * 处理 Validator 校验不通过产生的异常 |
| | | */ |
| | | @ExceptionHandler(value = ConstraintViolationException.class) |
| | | public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) { |
| | | log.warn("[constraintViolationExceptionHandler]", ex); |
| | | ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next(); |
| | | return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); |
| | | } |
| | | |
| | | /** |
| | | * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常 |
| | | */ |
| | | @ExceptionHandler(value = ValidationException.class) |
| | | public CommonResult<?> validationException(ValidationException ex) { |
| | | log.warn("[constraintViolationExceptionHandler]", ex); |
| | | // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读 |
| | | return CommonResult.error(BAD_REQUEST); |
| | | } |
| | | |
| | | /** |
| | | * 处理 SpringMVC 请求地址不存在 |
| | | * |
| | | * 注意,它需要设置如下两个配置项: |
| | | * 1. spring.mvc.throw-exception-if-no-handler-found 为 true |
| | | * 2. spring.mvc.static-path-pattern 为 /statics/** |
| | | */ |
| | | @ExceptionHandler(NoHandlerFoundException.class) |
| | | public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) { |
| | | log.warn("[noHandlerFoundExceptionHandler]", ex); |
| | | return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); |
| | | } |
| | | |
| | | /** |
| | | * 处理 SpringMVC 请求方法不正确 |
| | | * |
| | | * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配 |
| | | */ |
| | | @ExceptionHandler(HttpRequestMethodNotSupportedException.class) |
| | | public CommonResult<?> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { |
| | | log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex); |
| | | return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); |
| | | } |
| | | |
| | | /** |
| | | * 处理 Spring Security 权限不足的异常 |
| | | * |
| | | * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截 |
| | | */ |
| | | @ExceptionHandler(value = AccessDeniedException.class) |
| | | public CommonResult<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) { |
| | | log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req), |
| | | req.getRequestURL(), ex); |
| | | return CommonResult.error(FORBIDDEN); |
| | | } |
| | | |
| | | /** |
| | | * 处理业务异常 SQLIntegrityConstraintViolationException |
| | | * |
| | | * 数据库存在重复数据 |
| | | */ |
| | | @ExceptionHandler(value = DuplicateKeyException.class) |
| | | public CommonResult<?> duplicateKeyExceptionHandler(DuplicateKeyException ex) { |
| | | log.warn("[duplicateKeyExceptionHandler]", ex); |
| | | return CommonResult.error(DATA_REPETITION.getCode(), DATA_REPETITION.getMsg()); |
| | | } |
| | | |
| | | /** |
| | | * 处理业务异常 ServiceException |
| | | * |
| | | * 例如说,商品库存不足,用户手机号已存在。 |
| | | */ |
| | | @ExceptionHandler(value = ServiceException.class) |
| | | public CommonResult<?> serviceExceptionHandler(ServiceException ex) { |
| | | // 不包含的时候,才进行打印,避免 ex 堆栈过多 |
| | | if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) { |
| | | // 即使打印,也只打印第一层 StackTraceElement,并且使用 warn 在控制台输出,更容易看到 |
| | | StackTraceElement[] stackTrace = ex.getStackTrace(); |
| | | log.warn("[serviceExceptionHandler]\n\t{}", stackTrace[0]); |
| | | } |
| | | return CommonResult.error(ex.getCode(), ex.getMessage()); |
| | | } |
| | | |
| | | /** |
| | | * 处理系统异常,兜底处理所有的一切 |
| | | */ |
| | | @ExceptionHandler(value = Exception.class) |
| | | public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) { |
| | | // 情况一:处理表不存在的异常 |
| | | CommonResult<?> tableNotExistsResult = handleTableNotExists(ex); |
| | | if (tableNotExistsResult != null) { |
| | | return tableNotExistsResult; |
| | | } |
| | | |
| | | // 情况二:处理异常 |
| | | log.error("[defaultExceptionHandler]", ex); |
| | | // 插入异常日志 |
| | | createExceptionLog(req, ex); |
| | | // 返回 ERROR CommonResult |
| | | return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); |
| | | } |
| | | |
| | | private void createExceptionLog(HttpServletRequest req, Throwable e) { |
| | | // 插入错误日志 |
| | | ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO(); |
| | | try { |
| | | // 初始化 errorLog |
| | | buildExceptionLog(errorLog, req, e); |
| | | // 执行插入 errorLog |
| | | apiErrorLogFrameworkService.createApiErrorLog(errorLog); |
| | | } catch (Throwable th) { |
| | | log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); |
| | | } |
| | | } |
| | | |
| | | private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) { |
| | | // 处理用户信息 |
| | | errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); |
| | | errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); |
| | | // 设置异常字段 |
| | | errorLog.setExceptionName(e.getClass().getName()); |
| | | errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); |
| | | errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); |
| | | errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e)); |
| | | StackTraceElement[] stackTraceElements = e.getStackTrace(); |
| | | Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); |
| | | StackTraceElement stackTraceElement = stackTraceElements[0]; |
| | | errorLog.setExceptionClassName(stackTraceElement.getClassName()); |
| | | errorLog.setExceptionFileName(stackTraceElement.getFileName()); |
| | | errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); |
| | | errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); |
| | | // 设置其它字段 |
| | | errorLog.setTraceId(TracerUtils.getTraceId()); |
| | | errorLog.setApplicationName(applicationName); |
| | | errorLog.setRequestUrl(request.getRequestURI()); |
| | | Map<String, Object> requestParams = MapUtil.<String, Object>builder() |
| | | .put("query", ServletUtils.getParamMap(request)) |
| | | .put("body", ServletUtils.getBody(request)).build(); |
| | | errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); |
| | | errorLog.setRequestMethod(request.getMethod()); |
| | | errorLog.setUserAgent(ServletUtils.getUserAgent(request)); |
| | | errorLog.setUserIp(ServletUtils.getClientIP(request)); |
| | | errorLog.setExceptionTime(LocalDateTime.now()); |
| | | } |
| | | |
| | | /** |
| | | * 处理 Table 不存在的异常情况 |
| | | * |
| | | * @param ex 异常 |
| | | * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult |
| | | */ |
| | | private CommonResult<?> handleTableNotExists(Throwable ex) { |
| | | String message = ExceptionUtil.getRootCauseMessage(ex); |
| | | if (!message.contains("doesn't exist")) { |
| | | return null; |
| | | } |
| | | // 1. 数据报表 |
| | | if (message.contains("report_")) { |
| | | log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]"); |
| | | return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
| | | "[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]"); |
| | | } |
| | | // 2. 工作流 |
| | | if (message.contains("bpm_")) { |
| | | log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); |
| | | return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
| | | "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); |
| | | } |
| | | // 3. 微信公众号 |
| | | if (message.contains("mp_")) { |
| | | log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); |
| | | return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
| | | "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); |
| | | } |
| | | // 4. 商城系统 |
| | | if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) { |
| | | log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); |
| | | return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
| | | "[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); |
| | | } |
| | | // 5. ERP 系统 |
| | | if (message.contains("erp_")) { |
| | | log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); |
| | | return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
| | | "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); |
| | | } |
| | | // 6. CRM 系统 |
| | | if (message.contains("crm_")) { |
| | | log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); |
| | | return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
| | | "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); |
| | | } |
| | | // 7. 支付平台 |
| | | if (message.contains("pay_")) { |
| | | log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); |
| | | return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
| | | "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); |
| | | } |
| | | // 8. AI 大模型 |
| | | if (message.contains("ai_")) { |
| | | log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); |
| | | return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
| | | "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.web.core.handler; |
| | | |
| | | import com.iailab.framework.common.pojo.CommonResult; |
| | | import com.iailab.framework.web.core.util.WebFrameworkUtils; |
| | | import org.springframework.core.MethodParameter; |
| | | import org.springframework.http.MediaType; |
| | | import org.springframework.http.server.ServerHttpRequest; |
| | | import org.springframework.http.server.ServerHttpResponse; |
| | | import org.springframework.http.server.ServletServerHttpRequest; |
| | | import org.springframework.web.bind.annotation.ControllerAdvice; |
| | | import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; |
| | | |
| | | /** |
| | | * 全局响应结果(ResponseBody)处理器 |
| | | * |
| | | * 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult}, |
| | | * 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。 |
| | | * 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构 |
| | | * |
| | | * 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果, |
| | | * 方便 {@link com.iailab.framework.apilog.core.filter.ApiAccessLogFilter} 记录访问日志 |
| | | */ |
| | | @ControllerAdvice |
| | | public class GlobalResponseBodyHandler implements ResponseBodyAdvice { |
| | | |
| | | @Override |
| | | @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 |
| | | public boolean supports(MethodParameter returnType, Class converterType) { |
| | | if (returnType.getMethod() == null) { |
| | | return false; |
| | | } |
| | | // 只拦截返回结果为 CommonResult 类型 |
| | | return returnType.getMethod().getReturnType() == CommonResult.class; |
| | | } |
| | | |
| | | @Override |
| | | @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 |
| | | public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, |
| | | ServerHttpRequest request, ServerHttpResponse response) { |
| | | // 记录 Controller 结果 |
| | | WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult<?>) body); |
| | | return body; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.web.core.util; |
| | | |
| | | import cn.hutool.core.util.NumberUtil; |
| | | import cn.hutool.extra.servlet.ServletUtil; |
| | | import com.iailab.framework.common.enums.RpcConstants; |
| | | import com.iailab.framework.common.enums.TerminalEnum; |
| | | import com.iailab.framework.common.enums.UserTypeEnum; |
| | | import com.iailab.framework.common.pojo.CommonResult; |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import com.iailab.framework.web.config.WebProperties; |
| | | import org.springframework.web.context.request.RequestAttributes; |
| | | import org.springframework.web.context.request.RequestContextHolder; |
| | | import org.springframework.web.context.request.ServletRequestAttributes; |
| | | |
| | | import javax.servlet.ServletRequest; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | |
| | | /** |
| | | * 专属于 web 包的工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class WebFrameworkUtils { |
| | | |
| | | private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id"; |
| | | private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type"; |
| | | |
| | | private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; |
| | | |
| | | public static final String HEADER_TENANT_ID = "tenant-id"; |
| | | |
| | | /** |
| | | * 终端的 Header |
| | | * |
| | | * @see com.iailab.framework.common.enums.TerminalEnum |
| | | */ |
| | | public static final String HEADER_TERMINAL = "terminal"; |
| | | |
| | | private static WebProperties properties; |
| | | |
| | | public WebFrameworkUtils(WebProperties webProperties) { |
| | | WebFrameworkUtils.properties = webProperties; |
| | | } |
| | | |
| | | /** |
| | | * 获得租户编号,从 header 中 |
| | | * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供 |
| | | * |
| | | * @param request 请求 |
| | | * @return 租户编号 |
| | | */ |
| | | public static Long getTenantId(HttpServletRequest request) { |
| | | String tenantId = request.getHeader(HEADER_TENANT_ID); |
| | | return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null; |
| | | } |
| | | |
| | | public static void setLoginUserId(ServletRequest request, Long userId) { |
| | | request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); |
| | | } |
| | | |
| | | /** |
| | | * 设置用户类型 |
| | | * |
| | | * @param request 请求 |
| | | * @param userType 用户类型 |
| | | */ |
| | | public static void setLoginUserType(ServletRequest request, Integer userType) { |
| | | request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType); |
| | | } |
| | | |
| | | /** |
| | | * 获得当前用户的编号,从请求中 |
| | | * 注意:该方法仅限于 framework 框架使用!!! |
| | | * |
| | | * @param request 请求 |
| | | * @return 用户编号 |
| | | */ |
| | | public static Long getLoginUserId(HttpServletRequest request) { |
| | | if (request == null) { |
| | | return null; |
| | | } |
| | | return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID); |
| | | } |
| | | |
| | | /** |
| | | * 获得当前用户的类型 |
| | | * 注意:该方法仅限于 web 相关的 framework 组件使用!!! |
| | | * |
| | | * @param request 请求 |
| | | * @return 用户编号 |
| | | */ |
| | | public static Integer getLoginUserType(HttpServletRequest request) { |
| | | if (request == null) { |
| | | return null; |
| | | } |
| | | // 1. 优先,从 Attribute 中获取 |
| | | Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); |
| | | if (userType != null) { |
| | | return userType; |
| | | } |
| | | // 2. 其次,基于 URL 前缀的约定 |
| | | if (request.getServletPath().startsWith(properties.getAdminApi().getPrefix())) { |
| | | return UserTypeEnum.ADMIN.getValue(); |
| | | } |
| | | if (request.getServletPath().startsWith(properties.getAppApi().getPrefix())) { |
| | | return UserTypeEnum.MEMBER.getValue(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public static Integer getLoginUserType() { |
| | | HttpServletRequest request = getRequest(); |
| | | return getLoginUserType(request); |
| | | } |
| | | |
| | | public static Long getLoginUserId() { |
| | | HttpServletRequest request = getRequest(); |
| | | return getLoginUserId(request); |
| | | } |
| | | |
| | | public static Integer getTerminal() { |
| | | HttpServletRequest request = getRequest(); |
| | | if (request == null) { |
| | | return TerminalEnum.UNKNOWN.getTerminal(); |
| | | } |
| | | String terminalValue = request.getHeader(HEADER_TERMINAL); |
| | | return NumberUtil.parseInt(terminalValue, TerminalEnum.UNKNOWN.getTerminal()); |
| | | } |
| | | |
| | | public static void setCommonResult(ServletRequest request, CommonResult<?> result) { |
| | | request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result); |
| | | } |
| | | |
| | | public static CommonResult<?> getCommonResult(ServletRequest request) { |
| | | return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); |
| | | } |
| | | |
| | | public static HttpServletRequest getRequest() { |
| | | RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); |
| | | if (!(requestAttributes instanceof ServletRequestAttributes)) { |
| | | return null; |
| | | } |
| | | ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; |
| | | return servletRequestAttributes.getRequest(); |
| | | } |
| | | |
| | | /** |
| | | * 判断是否为 RPC 请求 |
| | | * |
| | | * @param request 请求 |
| | | * @return 是否为 RPC 请求 |
| | | */ |
| | | public static boolean isRpcRequest(HttpServletRequest request) { |
| | | return request.getRequestURI().startsWith(RpcConstants.RPC_API_PREFIX); |
| | | } |
| | | |
| | | /** |
| | | * 判断是否为 RPC 请求 |
| | | * |
| | | * 约定大于配置,只要以 Api 结尾,都认为是 RPC 接口 |
| | | * |
| | | * @param className 类名 |
| | | * @return 是否为 RPC 请求 |
| | | */ |
| | | public static boolean isRpcRequest(String className) { |
| | | return className.endsWith("Api"); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 针对 SpringMVC 的基础封装 |
| | | */ |
| | | package com.iailab.framework.web; |
对比新文件 |
| | |
| | | package com.iailab.framework.xss.config; |
| | | |
| | | import com.iailab.framework.common.enums.WebFilterOrderEnum; |
| | | import com.iailab.framework.xss.core.clean.JsoupXssCleaner; |
| | | import com.iailab.framework.xss.core.clean.XssCleaner; |
| | | import com.iailab.framework.xss.core.filter.XssFilter; |
| | | import com.iailab.framework.xss.core.json.XssStringJsonDeserializer; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.util.PathMatcher; |
| | | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; |
| | | |
| | | import static com.iailab.framework.web.config.IailabWebAutoConfiguration.createFilterBean; |
| | | |
| | | @AutoConfiguration |
| | | @EnableConfigurationProperties(XssProperties.class) |
| | | @ConditionalOnProperty(prefix = "iailab.xss", name = "enable", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用 |
| | | public class IailabXssAutoConfiguration implements WebMvcConfigurer { |
| | | |
| | | /** |
| | | * Xss 清理者 |
| | | * |
| | | * @return XssCleaner |
| | | */ |
| | | @Bean |
| | | @ConditionalOnMissingBean(XssCleaner.class) |
| | | public XssCleaner xssCleaner() { |
| | | return new JsoupXssCleaner(); |
| | | } |
| | | |
| | | /** |
| | | * 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤 |
| | | * |
| | | * @return Jackson2ObjectMapperBuilderCustomizer |
| | | */ |
| | | @Bean |
| | | @ConditionalOnMissingBean(name = "xssJacksonCustomizer") |
| | | @ConditionalOnBean(ObjectMapper.class) |
| | | @ConditionalOnProperty(value = "iailab.xss.enable", havingValue = "true") |
| | | public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties, |
| | | PathMatcher pathMatcher, |
| | | XssCleaner xssCleaner) { |
| | | // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理 |
| | | return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner)); |
| | | } |
| | | |
| | | /** |
| | | * 创建 XssFilter Bean,解决 Xss 安全问题 |
| | | */ |
| | | @Bean |
| | | @ConditionalOnBean(XssCleaner.class) |
| | | public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) { |
| | | return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.xss.config; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * Xss 配置属性 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @ConfigurationProperties(prefix = "iailab.xss") |
| | | @Validated |
| | | @Data |
| | | public class XssProperties { |
| | | |
| | | /** |
| | | * 是否开启,默认为 true |
| | | */ |
| | | private boolean enable = true; |
| | | /** |
| | | * 需要排除的 URL,默认为空 |
| | | */ |
| | | private List<String> excludeUrls = Collections.emptyList(); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.xss.core.clean; |
| | | |
| | | import org.jsoup.Jsoup; |
| | | import org.jsoup.nodes.Document; |
| | | import org.jsoup.safety.Safelist; |
| | | |
| | | /** |
| | | * 基于 JSONP 实现 XSS 过滤字符串 |
| | | */ |
| | | public class JsoupXssCleaner implements XssCleaner { |
| | | |
| | | private final Safelist safelist; |
| | | |
| | | /** |
| | | * 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分) |
| | | */ |
| | | private final String baseUri; |
| | | |
| | | /** |
| | | * 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表 |
| | | */ |
| | | public JsoupXssCleaner() { |
| | | this.safelist = buildSafelist(); |
| | | this.baseUri = ""; |
| | | } |
| | | |
| | | /** |
| | | * 构建一个 Xss 清理的 Safelist 规则。 |
| | | * 基于 Safelist#relaxed() 的基础上: |
| | | * 1. 扩展支持了 style 和 class 属性 |
| | | * 2. a 标签额外支持了 target 属性 |
| | | * 3. img 标签额外支持了 data 协议,便于支持 base64 |
| | | * |
| | | * @return Safelist |
| | | */ |
| | | private Safelist buildSafelist() { |
| | | // 使用 jsoup 提供的默认的 |
| | | Safelist relaxedSafelist = Safelist.relaxed(); |
| | | // 富文本编辑时一些样式是使用 style 来进行实现的 |
| | | // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性 |
| | | // 注意:style 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))"> |
| | | relaxedSafelist.addAttributes(":all", "style", "class"); |
| | | // 保留 a 标签的 target 属性 |
| | | relaxedSafelist.addAttributes("a", "target"); |
| | | // 支持img 为base64 |
| | | relaxedSafelist.addProtocols("img", "src", "data"); |
| | | |
| | | // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除 |
| | | // WHITELIST.preserveRelativeLinks(false); |
| | | |
| | | // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 <img src=javascript:alert("xss")> |
| | | // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径 |
| | | // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto"); |
| | | // WHITELIST.removeProtocols("img", "src", "http", "https"); |
| | | return relaxedSafelist; |
| | | } |
| | | |
| | | @Override |
| | | public String clean(String html) { |
| | | return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false)); |
| | | } |
| | | |
| | | } |
| | | |
对比新文件 |
| | |
| | | package com.iailab.framework.xss.core.clean; |
| | | |
| | | /** |
| | | * 对 html 文本中的有 Xss 风险的数据进行清理 |
| | | */ |
| | | public interface XssCleaner { |
| | | |
| | | /** |
| | | * 清理有 Xss 风险的文本 |
| | | * |
| | | * @param html 原 html |
| | | * @return 清理后的 html |
| | | */ |
| | | String clean(String html); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.xss.core.filter; |
| | | |
| | | import com.iailab.framework.xss.config.XssProperties; |
| | | import com.iailab.framework.xss.core.clean.XssCleaner; |
| | | import lombok.AllArgsConstructor; |
| | | import org.springframework.util.PathMatcher; |
| | | import org.springframework.web.filter.OncePerRequestFilter; |
| | | |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | |
| | | /** |
| | | * Xss 过滤器 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AllArgsConstructor |
| | | public class XssFilter extends OncePerRequestFilter { |
| | | |
| | | /** |
| | | * 属性 |
| | | */ |
| | | private final XssProperties properties; |
| | | /** |
| | | * 路径匹配器 |
| | | */ |
| | | private final PathMatcher pathMatcher; |
| | | |
| | | private final XssCleaner xssCleaner; |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
| | | throws IOException, ServletException { |
| | | filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response); |
| | | } |
| | | |
| | | @Override |
| | | protected boolean shouldNotFilter(HttpServletRequest request) { |
| | | // 如果关闭,则不过滤 |
| | | if (!properties.isEnable()) { |
| | | return true; |
| | | } |
| | | |
| | | // 如果匹配到无需过滤,则不过滤 |
| | | String uri = request.getRequestURI(); |
| | | return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.xss.core.filter; |
| | | |
| | | import com.iailab.framework.xss.core.clean.XssCleaner; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletRequestWrapper; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * Xss 请求 Wrapper |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class XssRequestWrapper extends HttpServletRequestWrapper { |
| | | |
| | | private final XssCleaner xssCleaner; |
| | | |
| | | public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) { |
| | | super(request); |
| | | this.xssCleaner = xssCleaner; |
| | | } |
| | | |
| | | // ============================ parameter ============================ |
| | | @Override |
| | | public Map<String, String[]> getParameterMap() { |
| | | Map<String, String[]> map = new LinkedHashMap<>(); |
| | | Map<String, String[]> parameters = super.getParameterMap(); |
| | | for (Map.Entry<String, String[]> entry : parameters.entrySet()) { |
| | | String[] values = entry.getValue(); |
| | | for (int i = 0; i < values.length; i++) { |
| | | values[i] = xssCleaner.clean(values[i]); |
| | | } |
| | | map.put(entry.getKey(), values); |
| | | } |
| | | return map; |
| | | } |
| | | |
| | | @Override |
| | | public String[] getParameterValues(String name) { |
| | | String[] values = super.getParameterValues(name); |
| | | if (values == null) { |
| | | return null; |
| | | } |
| | | int count = values.length; |
| | | String[] encodedValues = new String[count]; |
| | | for (int i = 0; i < count; i++) { |
| | | encodedValues[i] = xssCleaner.clean(values[i]); |
| | | } |
| | | return encodedValues; |
| | | } |
| | | |
| | | @Override |
| | | public String getParameter(String name) { |
| | | String value = super.getParameter(name); |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | return xssCleaner.clean(value); |
| | | } |
| | | |
| | | // ============================ attribute ============================ |
| | | @Override |
| | | public Object getAttribute(String name) { |
| | | Object value = super.getAttribute(name); |
| | | if (value instanceof String) { |
| | | return xssCleaner.clean((String) value); |
| | | } |
| | | return value; |
| | | } |
| | | |
| | | // ============================ header ============================ |
| | | @Override |
| | | public String getHeader(String name) { |
| | | String value = super.getHeader(name); |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | return xssCleaner.clean(value); |
| | | } |
| | | |
| | | // ============================ queryString ============================ |
| | | @Override |
| | | public String getQueryString() { |
| | | String value = super.getQueryString(); |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | return xssCleaner.clean(value); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.xss.core.json; |
| | | |
| | | import com.iailab.framework.common.util.servlet.ServletUtils; |
| | | import com.iailab.framework.xss.config.XssProperties; |
| | | import com.iailab.framework.xss.core.clean.XssCleaner; |
| | | import com.fasterxml.jackson.core.JsonParser; |
| | | import com.fasterxml.jackson.core.JsonToken; |
| | | import com.fasterxml.jackson.databind.DeserializationContext; |
| | | import com.fasterxml.jackson.databind.deser.std.StringDeserializer; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.util.PathMatcher; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.io.IOException; |
| | | |
| | | /** |
| | | * XSS 过滤 jackson 反序列化器。 |
| | | * 在反序列化的过程中,会对字符串进行 XSS 过滤。 |
| | | * |
| | | * @author Hccake |
| | | */ |
| | | @Slf4j |
| | | @AllArgsConstructor |
| | | public class XssStringJsonDeserializer extends StringDeserializer { |
| | | |
| | | /** |
| | | * 属性 |
| | | */ |
| | | private final XssProperties properties; |
| | | /** |
| | | * 路径匹配器 |
| | | */ |
| | | private final PathMatcher pathMatcher; |
| | | |
| | | private final XssCleaner xssCleaner; |
| | | |
| | | @Override |
| | | public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { |
| | | // 1. 白名单 URL 的处理 |
| | | HttpServletRequest request = ServletUtils.getRequest(); |
| | | if (request != null) { |
| | | String uri = ServletUtils.getRequest().getRequestURI(); |
| | | if (properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri))) { |
| | | return p.getText(); |
| | | } |
| | | } |
| | | |
| | | // 2. 真正使用 xssCleaner 进行过滤 |
| | | if (p.hasToken(JsonToken.VALUE_STRING)) { |
| | | return xssCleaner.clean(p.getText()); |
| | | } |
| | | JsonToken t = p.currentToken(); |
| | | // [databind#381] |
| | | if (t == JsonToken.START_ARRAY) { |
| | | return _deserializeFromArray(p, ctxt); |
| | | } |
| | | // need to gracefully handle byte[] data, as base64 |
| | | if (t == JsonToken.VALUE_EMBEDDED_OBJECT) { |
| | | Object ob = p.getEmbeddedObject(); |
| | | if (ob == null) { |
| | | return null; |
| | | } |
| | | if (ob instanceof byte[]) { |
| | | return ctxt.getBase64Variant().encode((byte[]) ob, false); |
| | | } |
| | | // otherwise, try conversion using toString()... |
| | | return ob.toString(); |
| | | } |
| | | // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) |
| | | if (t == JsonToken.START_OBJECT) { |
| | | return ctxt.extractScalarFromObject(p, this, _valueClass); |
| | | } |
| | | |
| | | if (t.isScalarValue()) { |
| | | String text = p.getValueAsString(); |
| | | return xssCleaner.clean(text); |
| | | } |
| | | return (String) ctxt.handleUnexpectedToken(_valueClass, p); |
| | | } |
| | | } |
| | | |
对比新文件 |
| | |
| | | /** |
| | | * 针对 XSS 的基础封装 |
| | | * |
| | | * XSS 说明:https://tech.meituan.com/2018/09/27/fe-security.html |
| | | */ |
| | | package com.iailab.framework.xss; |
对比新文件 |
| | |
| | | com.iailab.framework.apilog.config.IailabApiLogAutoConfiguration |
| | | com.iailab.framework.jackson.config.IailabJacksonAutoConfiguration |
| | | com.iailab.framework.swagger.config.IailabSwaggerAutoConfiguration |
| | | com.iailab.framework.web.config.IailabWebAutoConfiguration |
| | | com.iailab.framework.apilog.config.IailabApiLogRpcAutoConfiguration |
| | | com.iailab.framework.banner.config.IailabBannerAutoConfiguration |
对比新文件 |
| | |
| | | iailab |
| | | Application Version: ${iailab.info.version} |
| | | Spring Boot Version: ${spring-boot.version} |
| | | |
| | | ██ ██ ██ ██ ██ ██ ████ |
| | | ░░ ░░ ░██ ░██ ██████ ░██ ░██ ░██░ |
| | | ██ ██████ ██ ░██ ██████ ░██ ░██░░░██ ░██ ██████ ██████ ██████ ██████ ██████ ██████████ |
| | | ░██ ░░░░░░██ ░██ ░██ ░░░░░░██ ░██████ ░██ ░██ ░██ ░░░░░░██ ░░░██░ ░░░██░ ██░░░░██░░██░░█░░██░░██░░██ |
| | | ░██ ███████ ░██ ░██ ███████ ░██░░░██ ░██████ ░██ ███████ ░██ ░██ ░██ ░██ ░██ ░ ░██ ░██ ░██ |
| | | ░██ ██░░░░██ ░██ ░██ ██░░░░██ ░██ ░██ ░██░░░ ░██ ██░░░░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ |
| | | ░██░░████████░██ ███░░████████░██████ ░██ ███░░████████ ░░██ ░██ ░░██████ ░███ ███ ░██ ░██ |
| | | ░░ ░░░░░░░░ ░░ ░░░ ░░░░░░░░ ░░░░░ ░░ ░░░ ░░░░░░░░ ░░ ░░ ░░░░░░ ░░░ ░░░ ░░ ░░ |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core; |
| | | |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.desensitize.core.regex.annotation.EmailDesensitize; |
| | | import com.iailab.framework.desensitize.core.regex.annotation.RegexDesensitize; |
| | | import com.iailab.framework.desensitize.core.annotation.Address; |
| | | import com.iailab.framework.desensitize.core.slider.annotation.BankCardDesensitize; |
| | | import com.iailab.framework.desensitize.core.slider.annotation.CarLicenseDesensitize; |
| | | import com.iailab.framework.desensitize.core.slider.annotation.ChineseNameDesensitize; |
| | | import com.iailab.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize; |
| | | import com.iailab.framework.desensitize.core.slider.annotation.IdCardDesensitize; |
| | | import com.iailab.framework.desensitize.core.slider.annotation.PasswordDesensitize; |
| | | import com.iailab.framework.desensitize.core.slider.annotation.MobileDesensitize; |
| | | import com.iailab.framework.desensitize.core.slider.annotation.SliderDesensitize; |
| | | import lombok.Data; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.junit.jupiter.api.extension.ExtendWith; |
| | | import org.mockito.junit.jupiter.MockitoExtension; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | |
| | | /** |
| | | * {@link DesensitizeTest} 的单元测试 |
| | | */ |
| | | @ExtendWith(MockitoExtension.class) |
| | | public class DesensitizeTest { |
| | | |
| | | @Test |
| | | public void test() { |
| | | // 准备参数 |
| | | DesensitizeDemo desensitizeDemo = new DesensitizeDemo(); |
| | | desensitizeDemo.setNickname("iailab"); |
| | | desensitizeDemo.setBankCard("9988002866797031"); |
| | | desensitizeDemo.setCarLicense("粤A66666"); |
| | | desensitizeDemo.setFixedPhone("01086551122"); |
| | | desensitizeDemo.setIdCard("530321199204074611"); |
| | | desensitizeDemo.setPassword("123456"); |
| | | desensitizeDemo.setPhoneNumber("13248765917"); |
| | | desensitizeDemo.setSlider1("ABCDEFG"); |
| | | desensitizeDemo.setSlider2("ABCDEFG"); |
| | | desensitizeDemo.setSlider3("ABCDEFG"); |
| | | desensitizeDemo.setEmail("1@email.com"); |
| | | desensitizeDemo.setRegex("你好,我是iailab"); |
| | | desensitizeDemo.setAddress("北京市海淀区上地十街10号"); |
| | | desensitizeDemo.setOrigin("iailab"); |
| | | |
| | | // 调用 |
| | | DesensitizeDemo d = JsonUtils.parseObject(JsonUtils.toJsonString(desensitizeDemo), DesensitizeDemo.class); |
| | | // 断言 |
| | | assertNotNull(d); |
| | | assertEquals("芋***", d.getNickname()); |
| | | assertEquals("998800********31", d.getBankCard()); |
| | | assertEquals("粤A6***6", d.getCarLicense()); |
| | | assertEquals("0108*****22", d.getFixedPhone()); |
| | | assertEquals("530321**********11", d.getIdCard()); |
| | | assertEquals("******", d.getPassword()); |
| | | assertEquals("132****5917", d.getPhoneNumber()); |
| | | assertEquals("#######", d.getSlider1()); |
| | | assertEquals("ABC*EFG", d.getSlider2()); |
| | | assertEquals("*******", d.getSlider3()); |
| | | assertEquals("1****@email.com", d.getEmail()); |
| | | assertEquals("你好,我是*", d.getRegex()); |
| | | assertEquals("北京市海淀区上地十街10号*", d.getAddress()); |
| | | assertEquals("iailab", d.getOrigin()); |
| | | } |
| | | |
| | | @Data |
| | | public static class DesensitizeDemo { |
| | | |
| | | @ChineseNameDesensitize |
| | | private String nickname; |
| | | @BankCardDesensitize |
| | | private String bankCard; |
| | | @CarLicenseDesensitize |
| | | private String carLicense; |
| | | @FixedPhoneDesensitize |
| | | private String fixedPhone; |
| | | @IdCardDesensitize |
| | | private String idCard; |
| | | @PasswordDesensitize |
| | | private String password; |
| | | @MobileDesensitize |
| | | private String phoneNumber; |
| | | @SliderDesensitize(prefixKeep = 6, suffixKeep = 1, replacer = "#") |
| | | private String slider1; |
| | | @SliderDesensitize(prefixKeep = 3, suffixKeep = 3) |
| | | private String slider2; |
| | | @SliderDesensitize(prefixKeep = 10) |
| | | private String slider3; |
| | | @EmailDesensitize |
| | | private String email; |
| | | @RegexDesensitize(regex = "iailab", replacer = "*") |
| | | private String regex; |
| | | @Address |
| | | private String address; |
| | | private String origin; |
| | | |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.annotation; |
| | | |
| | | import com.iailab.framework.desensitize.core.DesensitizeTest; |
| | | import com.iailab.framework.desensitize.core.base.annotation.DesensitizeBy; |
| | | import com.iailab.framework.desensitize.core.handler.AddressHandler; |
| | | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; |
| | | |
| | | import java.lang.annotation.Documented; |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 地址 |
| | | * |
| | | * 用于 {@link DesensitizeTest} 测试使用 |
| | | * |
| | | * @author gaibu |
| | | */ |
| | | @Documented |
| | | @Target({ElementType.FIELD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @JacksonAnnotationsInside |
| | | @DesensitizeBy(handler = AddressHandler.class) |
| | | public @interface Address { |
| | | |
| | | String replacer() default "*"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.desensitize.core.handler; |
| | | |
| | | import com.iailab.framework.desensitize.core.DesensitizeTest; |
| | | import com.iailab.framework.desensitize.core.base.handler.DesensitizationHandler; |
| | | import com.iailab.framework.desensitize.core.annotation.Address; |
| | | |
| | | /** |
| | | * {@link Address} 的脱敏处理器 |
| | | * |
| | | * 用于 {@link DesensitizeTest} 测试使用 |
| | | */ |
| | | public class AddressHandler implements DesensitizationHandler<Address> { |
| | | |
| | | @Override |
| | | public String desensitize(String origin, Address annotation) { |
| | | return origin + annotation.replacer(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common-websocket</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>WebSocket 框架,支持多节点的广播</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- Web 相关 --> |
| | | <dependency> |
| | | <!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢? |
| | | 因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。 |
| | | 如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。 |
| | | --> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-security</artifactId> |
| | | <scope>provided</scope> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-websocket</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- 消息队列相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-mq</artifactId> |
| | | </dependency> |
| | | <!-- <dependency>--> |
| | | <!-- <groupId>org.springframework.kafka</groupId>--> |
| | | <!-- <artifactId>spring-kafka</artifactId>--> |
| | | <!-- <optional>true</optional>--> |
| | | <!-- </dependency>--> |
| | | <!-- <dependency>--> |
| | | <!-- <groupId>org.springframework.amqp</groupId>--> |
| | | <!-- <artifactId>spring-rabbit</artifactId>--> |
| | | <!-- <optional>true</optional>--> |
| | | <!-- </dependency>--> |
| | | <!-- <dependency>--> |
| | | <!-- <groupId>org.apache.rocketmq</groupId>--> |
| | | <!-- <artifactId>rocketmq-spring-boot-starter</artifactId>--> |
| | | <!-- <optional>true</optional>--> |
| | | <!-- </dependency>--> |
| | | |
| | | <!-- 业务组件 --> |
| | | <dependency> |
| | | <!-- 为什么要依赖 tenant 组件? |
| | | 因为广播某个类型的用户时候,需要根据租户过滤下,避免广播到别的租户! |
| | | --> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-biz-tenant</artifactId> |
| | | <scope>provided</scope> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.config; |
| | | |
| | | import com.iailab.framework.mq.redis.config.IailabRedisMQConsumerAutoConfiguration; |
| | | import com.iailab.framework.mq.redis.core.RedisMQTemplate; |
| | | import com.iailab.framework.websocket.core.handler.JsonWebSocketMessageHandler; |
| | | import com.iailab.framework.websocket.core.listener.WebSocketMessageListener; |
| | | import com.iailab.framework.websocket.core.security.LoginUserHandshakeInterceptor; |
| | | import com.iailab.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer; |
| | | import com.iailab.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.sender.local.LocalWebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer; |
| | | import com.iailab.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer; |
| | | import com.iailab.framework.websocket.core.sender.redis.RedisWebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer; |
| | | import com.iailab.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.session.WebSocketSessionHandlerDecorator; |
| | | import com.iailab.framework.websocket.core.session.WebSocketSessionManager; |
| | | import com.iailab.framework.websocket.core.session.WebSocketSessionManagerImpl; |
| | | import org.apache.rocketmq.spring.core.RocketMQTemplate; |
| | | import org.springframework.amqp.core.TopicExchange; |
| | | import org.springframework.amqp.rabbit.core.RabbitTemplate; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.kafka.core.KafkaTemplate; |
| | | import org.springframework.web.socket.WebSocketHandler; |
| | | import org.springframework.web.socket.config.annotation.EnableWebSocket; |
| | | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; |
| | | import org.springframework.web.socket.server.HandshakeInterceptor; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * WebSocket 自动配置 |
| | | * |
| | | * @author xingyu4j |
| | | */ |
| | | @AutoConfiguration(before = IailabRedisMQConsumerAutoConfiguration.class) // before IailabRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer |
| | | @EnableWebSocket // 开启 websocket |
| | | @ConditionalOnProperty(prefix = "iailab.websocket", value = "enable", matchIfMissing = true) // 允许使用 iailab.websocket.enable=false 禁用 websocket |
| | | @EnableConfigurationProperties(WebSocketProperties.class) |
| | | public class IailabWebSocketAutoConfiguration { |
| | | |
| | | @Bean |
| | | public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors, |
| | | WebSocketHandler webSocketHandler, |
| | | WebSocketProperties webSocketProperties) { |
| | | return registry -> registry |
| | | // 添加 WebSocketHandler |
| | | .addHandler(webSocketHandler, webSocketProperties.getPath()) |
| | | .addInterceptors(handshakeInterceptors) |
| | | // 允许跨域,否则前端连接会直接断开 |
| | | .setAllowedOriginPatterns("*"); |
| | | } |
| | | |
| | | @Bean |
| | | public HandshakeInterceptor handshakeInterceptor() { |
| | | return new LoginUserHandshakeInterceptor(); |
| | | } |
| | | |
| | | @Bean |
| | | public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager, |
| | | List<? extends WebSocketMessageListener<?>> messageListeners) { |
| | | // 1. 创建 JsonWebSocketMessageHandler 对象,处理消息 |
| | | JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners); |
| | | // 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接 |
| | | return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager); |
| | | } |
| | | |
| | | @Bean |
| | | public WebSocketSessionManager webSocketSessionManager() { |
| | | return new WebSocketSessionManagerImpl(); |
| | | } |
| | | |
| | | // ==================== Sender 相关 ==================== |
| | | |
| | | @Configuration |
| | | @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true) |
| | | public class LocalWebSocketMessageSenderConfiguration { |
| | | |
| | | @Bean |
| | | public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) { |
| | | return new LocalWebSocketMessageSender(sessionManager); |
| | | } |
| | | |
| | | } |
| | | |
| | | @Configuration |
| | | @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true) |
| | | public class RedisWebSocketMessageSenderConfiguration { |
| | | |
| | | @Bean |
| | | public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager, |
| | | RedisMQTemplate redisMQTemplate) { |
| | | return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate); |
| | | } |
| | | |
| | | @Bean |
| | | public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer( |
| | | RedisWebSocketMessageSender redisWebSocketMessageSender) { |
| | | return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender); |
| | | } |
| | | |
| | | } |
| | | |
| | | @Configuration |
| | | @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true) |
| | | public class RocketMQWebSocketMessageSenderConfiguration { |
| | | |
| | | @Bean |
| | | public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender( |
| | | WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate, |
| | | @Value("${iailab.websocket.sender-rocketmq.topic}") String topic) { |
| | | return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic); |
| | | } |
| | | |
| | | @Bean |
| | | public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer( |
| | | RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) { |
| | | return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender); |
| | | } |
| | | |
| | | } |
| | | |
| | | @Configuration |
| | | @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true) |
| | | public class RabbitMQWebSocketMessageSenderConfiguration { |
| | | |
| | | @Bean |
| | | public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender( |
| | | WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate, |
| | | TopicExchange websocketTopicExchange) { |
| | | return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange); |
| | | } |
| | | |
| | | @Bean |
| | | public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer( |
| | | RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) { |
| | | return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender); |
| | | } |
| | | |
| | | /** |
| | | * 创建 Topic Exchange |
| | | */ |
| | | @Bean |
| | | public TopicExchange websocketTopicExchange(@Value("${iailab.websocket.sender-rabbitmq.exchange}") String exchange) { |
| | | return new TopicExchange(exchange, |
| | | true, // durable: 是否持久化 |
| | | false); // exclusive: 是否排它 |
| | | } |
| | | |
| | | } |
| | | |
| | | @Configuration |
| | | @ConditionalOnProperty(prefix = "iailab.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true) |
| | | public class KafkaWebSocketMessageSenderConfiguration { |
| | | |
| | | @Bean |
| | | public KafkaWebSocketMessageSender kafkaWebSocketMessageSender( |
| | | WebSocketSessionManager sessionManager, KafkaTemplate<Object, Object> kafkaTemplate, |
| | | @Value("${iailab.websocket.sender-kafka.topic}") String topic) { |
| | | return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic); |
| | | } |
| | | |
| | | @Bean |
| | | public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer( |
| | | KafkaWebSocketMessageSender kafkaWebSocketMessageSender) { |
| | | return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender); |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.config; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import javax.validation.constraints.NotEmpty; |
| | | import javax.validation.constraints.NotNull; |
| | | |
| | | /** |
| | | * WebSocket 配置项 |
| | | * |
| | | * @author xingyu4j |
| | | */ |
| | | @ConfigurationProperties("iailab.websocket") |
| | | @Data |
| | | @Validated |
| | | public class WebSocketProperties { |
| | | |
| | | /** |
| | | * WebSocket 的连接路径 |
| | | */ |
| | | @NotEmpty(message = "WebSocket 的连接路径不能为空") |
| | | private String path = "/ws"; |
| | | |
| | | /** |
| | | * 消息发送器的类型 |
| | | * |
| | | * 可选值:local、redis、rocketmq、kafka、rabbitmq |
| | | */ |
| | | @NotNull(message = "WebSocket 的消息发送者不能为空") |
| | | private String senderType = "local"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.handler; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.core.util.TypeUtil; |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.tenant.core.util.TenantUtils; |
| | | import com.iailab.framework.websocket.core.listener.WebSocketMessageListener; |
| | | import com.iailab.framework.websocket.core.message.JsonWebSocketMessage; |
| | | import com.iailab.framework.websocket.core.util.WebSocketFrameworkUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.web.socket.TextMessage; |
| | | import org.springframework.web.socket.WebSocketHandler; |
| | | import org.springframework.web.socket.WebSocketSession; |
| | | import org.springframework.web.socket.handler.TextWebSocketHandler; |
| | | |
| | | import java.lang.reflect.Type; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.function.Consumer; |
| | | |
| | | /** |
| | | * JSON 格式 {@link WebSocketHandler} 实现类 |
| | | * |
| | | * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class JsonWebSocketMessageHandler extends TextWebSocketHandler { |
| | | |
| | | /** |
| | | * type 与 WebSocketMessageListener 的映射 |
| | | */ |
| | | private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>(); |
| | | |
| | | @SuppressWarnings({"rawtypes", "unchecked"}) |
| | | public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) { |
| | | listenersList.forEach((Consumer<WebSocketMessageListener>) |
| | | listener -> listeners.put(listener.getType(), listener)); |
| | | } |
| | | |
| | | @Override |
| | | protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { |
| | | // 1.1 空消息,跳过 |
| | | if (message.getPayloadLength() == 0) { |
| | | return; |
| | | } |
| | | // 1.2 ping 心跳消息,直接返回 pong 消息。 |
| | | if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) { |
| | | session.sendMessage(new TextMessage("pong")); |
| | | return; |
| | | } |
| | | |
| | | // 2.1 解析消息 |
| | | try { |
| | | JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class); |
| | | if (jsonMessage == null) { |
| | | log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload()); |
| | | return; |
| | | } |
| | | if (StrUtil.isEmpty(jsonMessage.getType())) { |
| | | log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload()); |
| | | return; |
| | | } |
| | | // 2.2 获得对应的 WebSocketMessageListener |
| | | WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType()); |
| | | if (messageListener == null) { |
| | | log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload()); |
| | | return; |
| | | } |
| | | // 2.3 处理消息 |
| | | Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0); |
| | | Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type); |
| | | Long tenantId = WebSocketFrameworkUtils.getTenantId(session); |
| | | TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj)); |
| | | } catch (Throwable ex) { |
| | | log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload()); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.listener; |
| | | |
| | | import com.iailab.framework.websocket.core.message.JsonWebSocketMessage; |
| | | import org.springframework.web.socket.WebSocketSession; |
| | | |
| | | /** |
| | | * WebSocket 消息监听器接口 |
| | | * |
| | | * 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息 |
| | | * |
| | | * @param <T> 泛型,消息类型 |
| | | */ |
| | | public interface WebSocketMessageListener<T> { |
| | | |
| | | /** |
| | | * 处理消息 |
| | | * |
| | | * @param session Session |
| | | * @param message 消息 |
| | | */ |
| | | void onMessage(WebSocketSession session, T message); |
| | | |
| | | /** |
| | | * 获得消息类型 |
| | | * |
| | | * @see JsonWebSocketMessage#getType() |
| | | * @return 消息类型 |
| | | */ |
| | | String getType(); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.message; |
| | | |
| | | import com.iailab.framework.websocket.core.listener.WebSocketMessageListener; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | /** |
| | | * JSON 格式的 WebSocket 消息帧 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Data |
| | | public class JsonWebSocketMessage implements Serializable { |
| | | |
| | | /** |
| | | * 消息类型 |
| | | * |
| | | * 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类 |
| | | */ |
| | | private String type; |
| | | /** |
| | | * 消息内容 |
| | | * |
| | | * 要求 JSON 对象 |
| | | */ |
| | | private String content; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.security; |
| | | |
| | | import com.iailab.framework.security.core.LoginUser; |
| | | import com.iailab.framework.security.core.filter.TokenAuthenticationFilter; |
| | | import com.iailab.framework.security.core.util.SecurityFrameworkUtils; |
| | | import com.iailab.framework.websocket.core.util.WebSocketFrameworkUtils; |
| | | import org.springframework.http.server.ServerHttpRequest; |
| | | import org.springframework.http.server.ServerHttpResponse; |
| | | import org.springframework.web.socket.WebSocketHandler; |
| | | import org.springframework.web.socket.WebSocketSession; |
| | | import org.springframework.web.socket.server.HandshakeInterceptor; |
| | | |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 登录用户的 {@link HandshakeInterceptor} 实现类 |
| | | * |
| | | * 流程如下: |
| | | * 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过 |
| | | * 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class LoginUserHandshakeInterceptor implements HandshakeInterceptor { |
| | | |
| | | @Override |
| | | public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, |
| | | WebSocketHandler wsHandler, Map<String, Object> attributes) { |
| | | LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); |
| | | if (loginUser != null) { |
| | | WebSocketFrameworkUtils.setLoginUser(loginUser, attributes); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, |
| | | WebSocketHandler wsHandler, Exception exception) { |
| | | // do nothing |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.security; |
| | | |
| | | import com.iailab.framework.security.config.AuthorizeRequestsCustomizer; |
| | | import com.iailab.framework.websocket.config.WebSocketProperties; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
| | | import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; |
| | | |
| | | /** |
| | | * WebSocket 的权限自定义 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { |
| | | |
| | | private final WebSocketProperties webSocketProperties; |
| | | |
| | | @Override |
| | | public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) { |
| | | registry.requestMatchers(webSocketProperties.getPath()).permitAll(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.websocket.core.message.JsonWebSocketMessage; |
| | | import com.iailab.framework.websocket.core.session.WebSocketSessionManager; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.web.socket.TextMessage; |
| | | import org.springframework.web.socket.WebSocketSession; |
| | | |
| | | import java.io.IOException; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * WebSocketMessageSender 实现类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | @RequiredArgsConstructor |
| | | public abstract class AbstractWebSocketMessageSender implements WebSocketMessageSender { |
| | | |
| | | private final WebSocketSessionManager sessionManager; |
| | | |
| | | @Override |
| | | public void send(Integer userType, Long userId, String messageType, String messageContent) { |
| | | send(null, userType, userId, messageType, messageContent); |
| | | } |
| | | |
| | | @Override |
| | | public void send(Integer userType, String messageType, String messageContent) { |
| | | send(null, userType, null, messageType, messageContent); |
| | | } |
| | | |
| | | @Override |
| | | public void send(String sessionId, String messageType, String messageContent) { |
| | | send(sessionId, null, null, messageType, messageContent); |
| | | } |
| | | |
| | | /** |
| | | * 发送消息 |
| | | * |
| | | * @param sessionId Session 编号 |
| | | * @param userType 用户类型 |
| | | * @param userId 用户编号 |
| | | * @param messageType 消息类型 |
| | | * @param messageContent 消息内容 |
| | | */ |
| | | public void send(String sessionId, Integer userType, Long userId, String messageType, String messageContent) { |
| | | // 1. 获得 Session 列表 |
| | | List<WebSocketSession> sessions = Collections.emptyList(); |
| | | if (StrUtil.isNotEmpty(sessionId)) { |
| | | WebSocketSession session = sessionManager.getSession(sessionId); |
| | | if (session != null) { |
| | | sessions = Collections.singletonList(session); |
| | | } |
| | | } else if (userType != null && userId != null) { |
| | | sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType, userId); |
| | | } else if (userType != null) { |
| | | sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType); |
| | | } |
| | | if (CollUtil.isEmpty(sessions)) { |
| | | log.info("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]", |
| | | sessionId, userType, userId, messageType, messageContent); |
| | | } |
| | | // 2. 执行发送 |
| | | doSend(sessions, messageType, messageContent); |
| | | } |
| | | |
| | | /** |
| | | * 发送消息的具体实现 |
| | | * |
| | | * @param sessions Session 列表 |
| | | * @param messageType 消息类型 |
| | | * @param messageContent 消息内容 |
| | | */ |
| | | public void doSend(Collection<WebSocketSession> sessions, String messageType, String messageContent) { |
| | | JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent); |
| | | String payload = JsonUtils.toJsonString(message); // 关键,使用 JSON 序列化 |
| | | sessions.forEach(session -> { |
| | | // 1. 各种校验,保证 Session 可以被发送 |
| | | if (session == null) { |
| | | log.error("[doSend][session 为空, message({})]", message); |
| | | return; |
| | | } |
| | | if (!session.isOpen()) { |
| | | log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message); |
| | | return; |
| | | } |
| | | // 2. 执行发送 |
| | | try { |
| | | session.sendMessage(new TextMessage(payload)); |
| | | log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message); |
| | | } catch (IOException ex) { |
| | | log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender; |
| | | |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | |
| | | /** |
| | | * WebSocket 消息的发送器接口 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface WebSocketMessageSender { |
| | | |
| | | /** |
| | | * 发送消息给指定用户 |
| | | * |
| | | * @param userType 用户类型 |
| | | * @param userId 用户编号 |
| | | * @param messageType 消息类型 |
| | | * @param messageContent 消息内容,JSON 格式 |
| | | */ |
| | | void send(Integer userType, Long userId, String messageType, String messageContent); |
| | | |
| | | /** |
| | | * 发送消息给指定用户类型 |
| | | * |
| | | * @param userType 用户类型 |
| | | * @param messageType 消息类型 |
| | | * @param messageContent 消息内容,JSON 格式 |
| | | */ |
| | | void send(Integer userType, String messageType, String messageContent); |
| | | |
| | | /** |
| | | * 发送消息给指定 Session |
| | | * |
| | | * @param sessionId Session 编号 |
| | | * @param messageType 消息类型 |
| | | * @param messageContent 消息内容,JSON 格式 |
| | | */ |
| | | void send(String sessionId, String messageType, String messageContent); |
| | | |
| | | default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) { |
| | | send(userType, userId, messageType, JsonUtils.toJsonString(messageContent)); |
| | | } |
| | | |
| | | default void sendObject(Integer userType, String messageType, Object messageContent) { |
| | | send(userType, messageType, JsonUtils.toJsonString(messageContent)); |
| | | } |
| | | |
| | | default void sendObject(String sessionId, String messageType, Object messageContent) { |
| | | send(sessionId, messageType, JsonUtils.toJsonString(messageContent)); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.kafka; |
| | | |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * Kafka 广播 WebSocket 的消息 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Data |
| | | public class KafkaWebSocketMessage { |
| | | |
| | | /** |
| | | * Session 编号 |
| | | */ |
| | | private String sessionId; |
| | | /** |
| | | * 用户类型 |
| | | */ |
| | | private Integer userType; |
| | | /** |
| | | * 用户编号 |
| | | */ |
| | | private Long userId; |
| | | |
| | | /** |
| | | * 消息类型 |
| | | */ |
| | | private String messageType; |
| | | /** |
| | | * 消息内容 |
| | | */ |
| | | private String messageContent; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.kafka; |
| | | |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.amqp.rabbit.annotation.RabbitHandler; |
| | | import org.springframework.kafka.annotation.KafkaListener; |
| | | |
| | | /** |
| | | * {@link KafkaWebSocketMessage} 广播消息的消费者,真正把消息发送出去 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | public class KafkaWebSocketMessageConsumer { |
| | | |
| | | private final KafkaWebSocketMessageSender rabbitMQWebSocketMessageSender; |
| | | |
| | | @RabbitHandler |
| | | @KafkaListener( |
| | | topics = "${iailab.websocket.sender-kafka.topic}", |
| | | // 在 Group 上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Group 不同,以达到广播消费的目的 |
| | | groupId = "${iailab.websocket.sender-kafka.consumer-group}" + "-" + "#{T(java.util.UUID).randomUUID()}") |
| | | public void onMessage(KafkaWebSocketMessage message) { |
| | | rabbitMQWebSocketMessageSender.send(message.getSessionId(), |
| | | message.getUserType(), message.getUserId(), |
| | | message.getMessageType(), message.getMessageContent()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.kafka; |
| | | |
| | | import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.sender.WebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.session.WebSocketSessionManager; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.kafka.core.KafkaTemplate; |
| | | |
| | | import java.util.concurrent.ExecutionException; |
| | | |
| | | /** |
| | | * 基于 Kafka 的 {@link WebSocketMessageSender} 实现类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class KafkaWebSocketMessageSender extends AbstractWebSocketMessageSender { |
| | | |
| | | private final KafkaTemplate<Object, Object> kafkaTemplate; |
| | | |
| | | private final String topic; |
| | | |
| | | public KafkaWebSocketMessageSender(WebSocketSessionManager sessionManager, |
| | | KafkaTemplate<Object, Object> kafkaTemplate, |
| | | String topic) { |
| | | super(sessionManager); |
| | | this.kafkaTemplate = kafkaTemplate; |
| | | this.topic = topic; |
| | | } |
| | | |
| | | @Override |
| | | public void send(Integer userType, Long userId, String messageType, String messageContent) { |
| | | sendKafkaMessage(null, userId, userType, messageType, messageContent); |
| | | } |
| | | |
| | | @Override |
| | | public void send(Integer userType, String messageType, String messageContent) { |
| | | sendKafkaMessage(null, null, userType, messageType, messageContent); |
| | | } |
| | | |
| | | @Override |
| | | public void send(String sessionId, String messageType, String messageContent) { |
| | | sendKafkaMessage(sessionId, null, null, messageType, messageContent); |
| | | } |
| | | |
| | | /** |
| | | * 通过 Kafka 广播消息 |
| | | * |
| | | * @param sessionId Session 编号 |
| | | * @param userId 用户编号 |
| | | * @param userType 用户类型 |
| | | * @param messageType 消息类型 |
| | | * @param messageContent 消息内容 |
| | | */ |
| | | private void sendKafkaMessage(String sessionId, Long userId, Integer userType, |
| | | String messageType, String messageContent) { |
| | | KafkaWebSocketMessage mqMessage = new KafkaWebSocketMessage() |
| | | .setSessionId(sessionId).setUserId(userId).setUserType(userType) |
| | | .setMessageType(messageType).setMessageContent(messageContent); |
| | | try { |
| | | kafkaTemplate.send(topic, mqMessage).get(); |
| | | } catch (InterruptedException | ExecutionException e) { |
| | | log.error("[sendKafkaMessage][发送消息({}) 到 Kafka 失败]", mqMessage, e); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.local; |
| | | |
| | | import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.sender.WebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.session.WebSocketSessionManager; |
| | | |
| | | /** |
| | | * 本地的 {@link WebSocketMessageSender} 实现类 |
| | | * |
| | | * 注意:仅仅适合单机场景!!! |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class LocalWebSocketMessageSender extends AbstractWebSocketMessageSender { |
| | | |
| | | public LocalWebSocketMessageSender(WebSocketSessionManager sessionManager) { |
| | | super(sessionManager); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.rabbitmq; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | /** |
| | | * RabbitMQ 广播 WebSocket 的消息 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Data |
| | | public class RabbitMQWebSocketMessage implements Serializable { |
| | | |
| | | /** |
| | | * Session 编号 |
| | | */ |
| | | private String sessionId; |
| | | /** |
| | | * 用户类型 |
| | | */ |
| | | private Integer userType; |
| | | /** |
| | | * 用户编号 |
| | | */ |
| | | private Long userId; |
| | | |
| | | /** |
| | | * 消息类型 |
| | | */ |
| | | private String messageType; |
| | | /** |
| | | * 消息内容 |
| | | */ |
| | | private String messageContent; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.rabbitmq; |
| | | |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.amqp.core.ExchangeTypes; |
| | | import org.springframework.amqp.rabbit.annotation.*; |
| | | |
| | | /** |
| | | * {@link RabbitMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RabbitListener( |
| | | bindings = @QueueBinding( |
| | | value = @Queue( |
| | | // 在 Queue 的名字上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Queue 不同,以达到广播消费的目的 |
| | | name = "${iailab.websocket.sender-rabbitmq.queue}" + "-" + "#{T(java.util.UUID).randomUUID()}", |
| | | // Consumer 关闭时,该队列就可以被自动删除了 |
| | | autoDelete = "true" |
| | | ), |
| | | exchange = @Exchange( |
| | | name = "${iailab.websocket.sender-rabbitmq.exchange}", |
| | | type = ExchangeTypes.TOPIC, |
| | | declare = "false" |
| | | ) |
| | | ) |
| | | ) |
| | | @RequiredArgsConstructor |
| | | public class RabbitMQWebSocketMessageConsumer { |
| | | |
| | | private final RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender; |
| | | |
| | | @RabbitHandler |
| | | public void onMessage(RabbitMQWebSocketMessage message) { |
| | | rabbitMQWebSocketMessageSender.send(message.getSessionId(), |
| | | message.getUserType(), message.getUserId(), |
| | | message.getMessageType(), message.getMessageContent()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.rabbitmq; |
| | | |
| | | import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.sender.WebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.session.WebSocketSessionManager; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.amqp.core.TopicExchange; |
| | | import org.springframework.amqp.rabbit.core.RabbitTemplate; |
| | | |
| | | /** |
| | | * 基于 RabbitMQ 的 {@link WebSocketMessageSender} 实现类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class RabbitMQWebSocketMessageSender extends AbstractWebSocketMessageSender { |
| | | |
| | | private final RabbitTemplate rabbitTemplate; |
| | | |
| | | private final TopicExchange topicExchange; |
| | | |
| | | public RabbitMQWebSocketMessageSender(WebSocketSessionManager sessionManager, |
| | | RabbitTemplate rabbitTemplate, |
| | | TopicExchange topicExchange) { |
| | | super(sessionManager); |
| | | this.rabbitTemplate = rabbitTemplate; |
| | | this.topicExchange = topicExchange; |
| | | } |
| | | |
| | | @Override |
| | | public void send(Integer userType, Long userId, String messageType, String messageContent) { |
| | | sendRabbitMQMessage(null, userId, userType, messageType, messageContent); |
| | | } |
| | | |
| | | @Override |
| | | public void send(Integer userType, String messageType, String messageContent) { |
| | | sendRabbitMQMessage(null, null, userType, messageType, messageContent); |
| | | } |
| | | |
| | | @Override |
| | | public void send(String sessionId, String messageType, String messageContent) { |
| | | sendRabbitMQMessage(sessionId, null, null, messageType, messageContent); |
| | | } |
| | | |
| | | /** |
| | | * 通过 RabbitMQ 广播消息 |
| | | * |
| | | * @param sessionId Session 编号 |
| | | * @param userId 用户编号 |
| | | * @param userType 用户类型 |
| | | * @param messageType 消息类型 |
| | | * @param messageContent 消息内容 |
| | | */ |
| | | private void sendRabbitMQMessage(String sessionId, Long userId, Integer userType, |
| | | String messageType, String messageContent) { |
| | | RabbitMQWebSocketMessage mqMessage = new RabbitMQWebSocketMessage() |
| | | .setSessionId(sessionId).setUserId(userId).setUserType(userType) |
| | | .setMessageType(messageType).setMessageContent(messageContent); |
| | | rabbitTemplate.convertAndSend(topicExchange.getName(), null, mqMessage); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.redis; |
| | | |
| | | import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage; |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * Redis 广播 WebSocket 的消息 |
| | | */ |
| | | @Data |
| | | public class RedisWebSocketMessage extends AbstractRedisChannelMessage { |
| | | |
| | | /** |
| | | * Session 编号 |
| | | */ |
| | | private String sessionId; |
| | | /** |
| | | * 用户类型 |
| | | */ |
| | | private Integer userType; |
| | | /** |
| | | * 用户编号 |
| | | */ |
| | | private Long userId; |
| | | |
| | | /** |
| | | * 消息类型 |
| | | */ |
| | | private String messageType; |
| | | /** |
| | | * 消息内容 |
| | | */ |
| | | private String messageContent; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.redis; |
| | | |
| | | import com.iailab.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; |
| | | import lombok.RequiredArgsConstructor; |
| | | |
| | | /** |
| | | * {@link RedisWebSocketMessage} 广播消息的消费者,真正把消息发送出去 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | public class RedisWebSocketMessageConsumer extends AbstractRedisChannelMessageListener<RedisWebSocketMessage> { |
| | | |
| | | private final RedisWebSocketMessageSender redisWebSocketMessageSender; |
| | | |
| | | @Override |
| | | public void onMessage(RedisWebSocketMessage message) { |
| | | redisWebSocketMessageSender.send(message.getSessionId(), |
| | | message.getUserType(), message.getUserId(), |
| | | message.getMessageType(), message.getMessageContent()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.redis; |
| | | |
| | | import com.iailab.framework.mq.redis.core.RedisMQTemplate; |
| | | import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.sender.WebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.session.WebSocketSessionManager; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | /** |
| | | * 基于 Redis 的 {@link WebSocketMessageSender} 实现类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender { |
| | | |
| | | private final RedisMQTemplate redisMQTemplate; |
| | | |
| | | public RedisWebSocketMessageSender(WebSocketSessionManager sessionManager, |
| | | RedisMQTemplate redisMQTemplate) { |
| | | super(sessionManager); |
| | | this.redisMQTemplate = redisMQTemplate; |
| | | } |
| | | |
| | | @Override |
| | | public void send(Integer userType, Long userId, String messageType, String messageContent) { |
| | | sendRedisMessage(null, userId, userType, messageType, messageContent); |
| | | } |
| | | |
| | | @Override |
| | | public void send(Integer userType, String messageType, String messageContent) { |
| | | sendRedisMessage(null, null, userType, messageType, messageContent); |
| | | } |
| | | |
| | | @Override |
| | | public void send(String sessionId, String messageType, String messageContent) { |
| | | sendRedisMessage(sessionId, null, null, messageType, messageContent); |
| | | } |
| | | |
| | | /** |
| | | * 通过 Redis 广播消息 |
| | | * |
| | | * @param sessionId Session 编号 |
| | | * @param userId 用户编号 |
| | | * @param userType 用户类型 |
| | | * @param messageType 消息类型 |
| | | * @param messageContent 消息内容 |
| | | */ |
| | | private void sendRedisMessage(String sessionId, Long userId, Integer userType, |
| | | String messageType, String messageContent) { |
| | | RedisWebSocketMessage mqMessage = new RedisWebSocketMessage() |
| | | .setSessionId(sessionId).setUserId(userId).setUserType(userType) |
| | | .setMessageType(messageType).setMessageContent(messageContent); |
| | | redisMQTemplate.send(mqMessage); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.rocketmq; |
| | | |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * RocketMQ 广播 WebSocket 的消息 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Data |
| | | public class RocketMQWebSocketMessage { |
| | | |
| | | /** |
| | | * Session 编号 |
| | | */ |
| | | private String sessionId; |
| | | /** |
| | | * 用户类型 |
| | | */ |
| | | private Integer userType; |
| | | /** |
| | | * 用户编号 |
| | | */ |
| | | private Long userId; |
| | | |
| | | /** |
| | | * 消息类型 |
| | | */ |
| | | private String messageType; |
| | | /** |
| | | * 消息内容 |
| | | */ |
| | | private String messageContent; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.rocketmq; |
| | | |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.apache.rocketmq.spring.annotation.MessageModel; |
| | | import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; |
| | | import org.apache.rocketmq.spring.core.RocketMQListener; |
| | | |
| | | /** |
| | | * {@link RocketMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RocketMQMessageListener( // 重点:添加 @RocketMQMessageListener 注解,声明消费的 topic |
| | | topic = "${iailab.websocket.sender-rocketmq.topic}", |
| | | consumerGroup = "${iailab.websocket.sender-rocketmq.consumer-group}", |
| | | messageModel = MessageModel.BROADCASTING // 设置为广播模式,保证每个实例都能收到消息 |
| | | ) |
| | | @RequiredArgsConstructor |
| | | public class RocketMQWebSocketMessageConsumer implements RocketMQListener<RocketMQWebSocketMessage> { |
| | | |
| | | private final RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender; |
| | | |
| | | @Override |
| | | public void onMessage(RocketMQWebSocketMessage message) { |
| | | rocketMQWebSocketMessageSender.send(message.getSessionId(), |
| | | message.getUserType(), message.getUserId(), |
| | | message.getMessageType(), message.getMessageContent()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.sender.rocketmq; |
| | | |
| | | import com.iailab.framework.websocket.core.sender.AbstractWebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.sender.WebSocketMessageSender; |
| | | import com.iailab.framework.websocket.core.session.WebSocketSessionManager; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.rocketmq.spring.core.RocketMQTemplate; |
| | | |
| | | /** |
| | | * 基于 RocketMQ 的 {@link WebSocketMessageSender} 实现类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class RocketMQWebSocketMessageSender extends AbstractWebSocketMessageSender { |
| | | |
| | | private final RocketMQTemplate rocketMQTemplate; |
| | | |
| | | private final String topic; |
| | | |
| | | public RocketMQWebSocketMessageSender(WebSocketSessionManager sessionManager, |
| | | RocketMQTemplate rocketMQTemplate, |
| | | String topic) { |
| | | super(sessionManager); |
| | | this.rocketMQTemplate = rocketMQTemplate; |
| | | this.topic = topic; |
| | | } |
| | | |
| | | @Override |
| | | public void send(Integer userType, Long userId, String messageType, String messageContent) { |
| | | sendRocketMQMessage(null, userId, userType, messageType, messageContent); |
| | | } |
| | | |
| | | @Override |
| | | public void send(Integer userType, String messageType, String messageContent) { |
| | | sendRocketMQMessage(null, null, userType, messageType, messageContent); |
| | | } |
| | | |
| | | @Override |
| | | public void send(String sessionId, String messageType, String messageContent) { |
| | | sendRocketMQMessage(sessionId, null, null, messageType, messageContent); |
| | | } |
| | | |
| | | /** |
| | | * 通过 RocketMQ 广播消息 |
| | | * |
| | | * @param sessionId Session 编号 |
| | | * @param userId 用户编号 |
| | | * @param userType 用户类型 |
| | | * @param messageType 消息类型 |
| | | * @param messageContent 消息内容 |
| | | */ |
| | | private void sendRocketMQMessage(String sessionId, Long userId, Integer userType, |
| | | String messageType, String messageContent) { |
| | | RocketMQWebSocketMessage mqMessage = new RocketMQWebSocketMessage() |
| | | .setSessionId(sessionId).setUserId(userId).setUserType(userType) |
| | | .setMessageType(messageType).setMessageContent(messageContent); |
| | | rocketMQTemplate.syncSend(topic, mqMessage); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.session; |
| | | |
| | | import org.springframework.web.socket.CloseStatus; |
| | | import org.springframework.web.socket.WebSocketHandler; |
| | | import org.springframework.web.socket.WebSocketSession; |
| | | import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator; |
| | | import org.springframework.web.socket.handler.WebSocketHandlerDecorator; |
| | | |
| | | /** |
| | | * {@link WebSocketHandler} 的装饰类,实现了以下功能: |
| | | * |
| | | * 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理 |
| | | * 2. 封装 {@link WebSocketSession} 支持并发操作 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator { |
| | | |
| | | /** |
| | | * 发送时间的限制,单位:毫秒 |
| | | */ |
| | | private static final Integer SEND_TIME_LIMIT = 1000 * 5; |
| | | /** |
| | | * 发送消息缓冲上线,单位:bytes |
| | | */ |
| | | private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100; |
| | | |
| | | private final WebSocketSessionManager sessionManager; |
| | | |
| | | public WebSocketSessionHandlerDecorator(WebSocketHandler delegate, |
| | | WebSocketSessionManager sessionManager) { |
| | | super(delegate); |
| | | this.sessionManager = sessionManager; |
| | | } |
| | | |
| | | @Override |
| | | public void afterConnectionEstablished(WebSocketSession session) { |
| | | // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149 |
| | | session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT); |
| | | // 添加到 WebSocketSessionManager 中 |
| | | sessionManager.addSession(session); |
| | | } |
| | | |
| | | @Override |
| | | public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) { |
| | | sessionManager.removeSession(session); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.session; |
| | | |
| | | import org.springframework.web.socket.WebSocketSession; |
| | | |
| | | import java.util.Collection; |
| | | |
| | | /** |
| | | * {@link WebSocketSession} 管理器的接口 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface WebSocketSessionManager { |
| | | |
| | | /** |
| | | * 添加 Session |
| | | * |
| | | * @param session Session |
| | | */ |
| | | void addSession(WebSocketSession session); |
| | | |
| | | /** |
| | | * 移除 Session |
| | | * |
| | | * @param session Session |
| | | */ |
| | | void removeSession(WebSocketSession session); |
| | | |
| | | /** |
| | | * 获得指定编号的 Session |
| | | * |
| | | * @param id Session 编号 |
| | | * @return Session |
| | | */ |
| | | WebSocketSession getSession(String id); |
| | | |
| | | /** |
| | | * 获得指定用户类型的 Session 列表 |
| | | * |
| | | * @param userType 用户类型 |
| | | * @return Session 列表 |
| | | */ |
| | | Collection<WebSocketSession> getSessionList(Integer userType); |
| | | |
| | | /** |
| | | * 获得指定用户编号的 Session 列表 |
| | | * |
| | | * @param userType 用户类型 |
| | | * @param userId 用户编号 |
| | | * @return Session 列表 |
| | | */ |
| | | Collection<WebSocketSession> getSessionList(Integer userType, Long userId); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.session; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.iailab.framework.security.core.LoginUser; |
| | | import com.iailab.framework.tenant.core.context.TenantContextHolder; |
| | | import com.iailab.framework.websocket.core.util.WebSocketFrameworkUtils; |
| | | import org.springframework.web.socket.WebSocketSession; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.ConcurrentMap; |
| | | import java.util.concurrent.CopyOnWriteArrayList; |
| | | |
| | | /** |
| | | * 默认的 {@link WebSocketSessionManager} 实现类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class WebSocketSessionManagerImpl implements WebSocketSessionManager { |
| | | |
| | | /** |
| | | * id 与 WebSocketSession 映射 |
| | | * |
| | | * key:Session 编号 |
| | | */ |
| | | private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>(); |
| | | |
| | | /** |
| | | * user 与 WebSocketSession 映射 |
| | | * |
| | | * key1:用户类型 |
| | | * key2:用户编号 |
| | | */ |
| | | private final ConcurrentMap<Integer, ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>>> userSessions |
| | | = new ConcurrentHashMap<>(); |
| | | |
| | | @Override |
| | | public void addSession(WebSocketSession session) { |
| | | // 添加到 idSessions 中 |
| | | idSessions.put(session.getId(), session); |
| | | // 添加到 userSessions 中 |
| | | LoginUser user = WebSocketFrameworkUtils.getLoginUser(session); |
| | | if (user == null) { |
| | | return; |
| | | } |
| | | ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType()); |
| | | if (userSessionsMap == null) { |
| | | userSessionsMap = new ConcurrentHashMap<>(); |
| | | if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) { |
| | | userSessionsMap = userSessions.get(user.getUserType()); |
| | | } |
| | | } |
| | | CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId()); |
| | | if (sessions == null) { |
| | | sessions = new CopyOnWriteArrayList<>(); |
| | | if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) { |
| | | sessions = userSessionsMap.get(user.getId()); |
| | | } |
| | | } |
| | | sessions.add(session); |
| | | } |
| | | |
| | | @Override |
| | | public void removeSession(WebSocketSession session) { |
| | | // 移除从 idSessions 中 |
| | | idSessions.remove(session.getId()); |
| | | // 移除从 idSessions 中 |
| | | LoginUser user = WebSocketFrameworkUtils.getLoginUser(session); |
| | | if (user == null) { |
| | | return; |
| | | } |
| | | ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType()); |
| | | if (userSessionsMap == null) { |
| | | return; |
| | | } |
| | | CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId()); |
| | | sessions.removeIf(session0 -> session0.getId().equals(session.getId())); |
| | | if (CollUtil.isEmpty(sessions)) { |
| | | userSessionsMap.remove(user.getId(), sessions); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public WebSocketSession getSession(String id) { |
| | | return idSessions.get(id); |
| | | } |
| | | |
| | | @Override |
| | | public Collection<WebSocketSession> getSessionList(Integer userType) { |
| | | ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType); |
| | | if (CollUtil.isEmpty(userSessionsMap)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免扩容 |
| | | Long contextTenantId = TenantContextHolder.getTenantId(); |
| | | for (List<WebSocketSession> sessions : userSessionsMap.values()) { |
| | | if (CollUtil.isEmpty(sessions)) { |
| | | continue; |
| | | } |
| | | // 特殊:如果租户不匹配,则直接排除 |
| | | if (contextTenantId != null) { |
| | | Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0)); |
| | | if (!contextTenantId.equals(userTenantId)) { |
| | | continue; |
| | | } |
| | | } |
| | | result.addAll(sessions); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | @Override |
| | | public Collection<WebSocketSession> getSessionList(Integer userType, Long userId) { |
| | | ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType); |
| | | if (CollUtil.isEmpty(userSessionsMap)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(userId); |
| | | return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.websocket.core.util; |
| | | |
| | | import com.iailab.framework.security.core.LoginUser; |
| | | import org.springframework.web.socket.WebSocketSession; |
| | | |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 专属于 web 包的工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class WebSocketFrameworkUtils { |
| | | |
| | | public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER"; |
| | | |
| | | /** |
| | | * 设置当前用户 |
| | | * |
| | | * @param loginUser 登录用户 |
| | | * @param attributes Session |
| | | */ |
| | | public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) { |
| | | attributes.put(ATTRIBUTE_LOGIN_USER, loginUser); |
| | | } |
| | | |
| | | /** |
| | | * 获取当前用户 |
| | | * |
| | | * @return 当前用户 |
| | | */ |
| | | public static LoginUser getLoginUser(WebSocketSession session) { |
| | | return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER); |
| | | } |
| | | |
| | | /** |
| | | * 获得当前用户的编号 |
| | | * |
| | | * @return 用户编号 |
| | | */ |
| | | public static Long getLoginUserId(WebSocketSession session) { |
| | | LoginUser loginUser = getLoginUser(session); |
| | | return loginUser != null ? loginUser.getId() : null; |
| | | } |
| | | |
| | | /** |
| | | * 获得当前用户的类型 |
| | | * |
| | | * @return 用户编号 |
| | | */ |
| | | public static Integer getLoginUserType(WebSocketSession session) { |
| | | LoginUser loginUser = getLoginUser(session); |
| | | return loginUser != null ? loginUser.getUserType() : null; |
| | | } |
| | | |
| | | /** |
| | | * 获得当前用户的租户编号 |
| | | * |
| | | * @param session Session |
| | | * @return 租户编号 |
| | | */ |
| | | public static Long getTenantId(WebSocketSession session) { |
| | | LoginUser loginUser = getLoginUser(session); |
| | | return loginUser != null ? loginUser.getTenantId() : null; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * WebSocket 框架,支持多节点的广播 |
| | | */ |
| | | package com.iailab.framework.websocket; |
对比新文件 |
| | |
| | | com.iailab.framework.websocket.config.IailabWebSocketAutoConfiguration |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <parent> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-framework</artifactId> |
| | | <version>${revision}</version> |
| | | </parent> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <artifactId>iailab-common</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>定义基础 pojo 类、枚举、工具类等等</description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <!-- Spring 核心 --> |
| | | <dependency> |
| | | <groupId>org.springframework</groupId> |
| | | <artifactId>spring-core</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,只有工具类需要使用到 –>--> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework</groupId> |
| | | <artifactId>spring-expression</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,只有工具类需要使用到 –>--> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework</groupId> |
| | | <artifactId>spring-aop</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,只有工具类需要使用到 –>--> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.aspectj</groupId> |
| | | <artifactId>aspectjweaver</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,只有工具类需要使用到 –>--> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 --> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-configuration-processor</artifactId> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | |
| | | <!-- Web 相关 --> |
| | | <dependency> |
| | | <groupId>org.springframework</groupId> |
| | | <artifactId>spring-web</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,只有工具类需要使用到 –>--> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>jakarta.servlet</groupId> |
| | | <artifactId>jakarta.servlet-api</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,只有工具类需要使用到 –>--> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.springdoc</groupId> |
| | | <artifactId>springdoc-openapi-ui</artifactId> |
| | | <scope>provided</scope> |
| | | </dependency> |
| | | |
| | | <!-- 监控相关 --> |
| | | <dependency> |
| | | <groupId>org.apache.skywalking</groupId> |
| | | <artifactId>apm-toolkit-trace</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- 工具类相关 --> |
| | | <dependency> |
| | | <groupId>org.projectlombok</groupId> |
| | | <artifactId>lombok</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.mapstruct</groupId> |
| | | <artifactId>mapstruct</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.mapstruct</groupId> |
| | | <artifactId>mapstruct-jdk8</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher --> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.mapstruct</groupId> |
| | | <artifactId>mapstruct-processor</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.google.guava</groupId> |
| | | <artifactId>guava</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,只有工具类需要使用到 –>--> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.fasterxml.jackson.core</groupId> |
| | | <artifactId>jackson-databind</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,只有工具类需要使用到 –>--> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.fasterxml.jackson.core</groupId> |
| | | <artifactId>jackson-core</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,只有工具类需要使用到 –>--> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.fasterxml.jackson.datatype</groupId> |
| | | <artifactId>jackson-datatype-jsr310</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,只有工具类需要使用到 –>--> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.slf4j</groupId> |
| | | <artifactId>slf4j-api</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,只有工具类需要使用到 –>--> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>jakarta.validation</groupId> |
| | | <artifactId>jakarta.validation-api</artifactId> |
| | | <!-- <scope>provided</scope> <!– 设置为 provided,主要是 PageParam 使用到 –>--> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>cn.hutool</groupId> |
| | | <artifactId>hutool-all</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>joda-time</groupId> |
| | | <artifactId>joda-time</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.alibaba</groupId> |
| | | <artifactId>transmittable-thread-local</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.jsoup</groupId> |
| | | <artifactId>jsoup</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 --> |
| | | <artifactId>easy-trans-anno</artifactId> <!-- 默认引入的原因,方便 xxx-module-api 包使用 --> |
| | | </dependency> |
| | | |
| | | <!-- Test 测试相关 --> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-test</artifactId> |
| | | <scope>test</scope> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.fhs.trans.service; |
| | | |
| | | import com.fhs.core.trans.vo.VO; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 只有实现了这个接口的才能自动翻译 |
| | | * |
| | | * 为什么要赋值粘贴到 iailab-common 包下? |
| | | * 因为 AutoTransable 属于 easy-trans-service 下,无法方便的在 iailab-module-xxx-api 模块下使用 |
| | | * |
| | | * @author jackwang |
| | | * @since 2020-05-19 10:26:15 |
| | | */ |
| | | public interface AutoTransable<V extends VO> { |
| | | |
| | | /** |
| | | * 根据 ids 查询数据列表 |
| | | * |
| | | * 改方法已过期啦,请使用 selectByIds |
| | | * |
| | | * @param ids 编号数组 |
| | | * @return 数据列表 |
| | | */ |
| | | @Deprecated |
| | | default List<V> findByIds(List<? extends Object> ids){ |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | /** |
| | | * 根据 ids 查询 |
| | | * |
| | | * @param ids 编号数组 |
| | | * @return 数据列表 |
| | | */ |
| | | default List<V> selectByIds(List<? extends Object> ids){ |
| | | return this.findByIds(ids); |
| | | } |
| | | |
| | | /** |
| | | * 获取 db 中所有的数据 |
| | | * |
| | | * @return db 中所有的数据 |
| | | */ |
| | | default List<V> select(){ |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | /** |
| | | * 根据 id 获取 vo |
| | | * |
| | | * @param primaryValue id |
| | | * @return vo |
| | | */ |
| | | V selectById(Object primaryValue); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.annotation; |
| | | |
| | | import java.lang.annotation.*; |
| | | |
| | | /** |
| | | * 业务流程 |
| | | * |
| | | * @author PanZhibao |
| | | * @Description |
| | | * @createTime 2022年12月20日 16:10:00 |
| | | */ |
| | | @Target(ElementType.METHOD) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Documented |
| | | public @interface AutoBpm { |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.annotation; |
| | | |
| | | import java.lang.annotation.*; |
| | | |
| | | /** |
| | | * @author PanZhibao |
| | | * @Description |
| | | * @createTime 2022年05月21日 10:59:00 |
| | | */ |
| | | @Target(ElementType.METHOD) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Documented |
| | | public @interface AutoDict { |
| | | |
| | | /** |
| | | * 暂时无用 |
| | | * @return |
| | | */ |
| | | String value() default ""; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.annotation; |
| | | |
| | | import java.lang.annotation.*; |
| | | |
| | | /** |
| | | * @author PanZhibao |
| | | * @Description |
| | | * @createTime 2023年06月07日 11:35:00 |
| | | */ |
| | | @Target(ElementType.METHOD) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Documented |
| | | public @interface AutoUser { |
| | | |
| | | /** |
| | | * 暂时无用 |
| | | * @return |
| | | */ |
| | | String value() default ""; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.annotation; |
| | | |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 业务流程ID |
| | | * |
| | | * @author PanZhibao |
| | | * @Description |
| | | * @createTime 2022年12月20日 16:17:00 |
| | | */ |
| | | @Target(ElementType.FIELD) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | public @interface BpmProcess { |
| | | |
| | | /** |
| | | * 业务ID |
| | | * |
| | | * @return |
| | | */ |
| | | String businessKey(); |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.annotation; |
| | | |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 业务流程状态 |
| | | * |
| | | * @author PanZhibao |
| | | * @Description |
| | | * @createTime 2022年12月20日 16:14:00 |
| | | */ |
| | | @Target(ElementType.FIELD) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | public @interface BpmStatus { |
| | | |
| | | /** |
| | | * 业务ID |
| | | * |
| | | * @return |
| | | */ |
| | | String businessKey(); |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.annotation; |
| | | |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * 字典注解 |
| | | * |
| | | * @author PanZhibao |
| | | * @Description |
| | | * @createTime 2022年05月20日 17:36:00 |
| | | */ |
| | | @Target(ElementType.FIELD) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | public @interface Dict { |
| | | |
| | | /** |
| | | * 数据code |
| | | * |
| | | * @return |
| | | */ |
| | | String dicCode(); |
| | | |
| | | /** |
| | | * 数据itemValue |
| | | * |
| | | * @return |
| | | */ |
| | | String itemValue(); |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.annotation; |
| | | |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * @author PanZhibao |
| | | * @Description |
| | | * @createTime 2023年06月07日 11:37:00 |
| | | */ |
| | | @Target(ElementType.FIELD) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | public @interface UserRealName { |
| | | |
| | | /** |
| | | * 用户ID |
| | | * |
| | | * @return |
| | | */ |
| | | String userid() default ""; |
| | | |
| | | /** |
| | | * 用户账号 |
| | | * |
| | | * @return |
| | | */ |
| | | String username() default ""; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.constant; |
| | | |
| | | /** |
| | | * @author: huangxutao |
| | | * @date: 2019-06-14 |
| | | * @description: 缓存常量 |
| | | */ |
| | | public interface CacheConstant { |
| | | |
| | | /** |
| | | * 字典信息缓存(含禁用的字典项) |
| | | */ |
| | | public static final String SYS_DICT_CACHE = "sys:cache:dict"; |
| | | |
| | | /** |
| | | * 字典信息缓存 status为有效的 |
| | | */ |
| | | public static final String SYS_ENABLE_DICT_CACHE = "sys:cache:dictEnable"; |
| | | /** |
| | | * 表字典信息缓存 |
| | | */ |
| | | public static final String SYS_DICT_TABLE_CACHE = "sys:cache:dictTable"; |
| | | public static final String SYS_DICT_TABLE_BY_KEYS_CACHE = SYS_DICT_TABLE_CACHE + "ByKeys"; |
| | | |
| | | /** |
| | | * 数据权限配置缓存 |
| | | */ |
| | | public static final String SYS_DATA_PERMISSIONS_CACHE = "sys:cache:permission:datarules"; |
| | | |
| | | /** |
| | | * 缓存用户信息 |
| | | */ |
| | | public static final String SYS_USERS_CACHE = "sys:cache:user"; |
| | | |
| | | /** |
| | | * 全部部门信息缓存 |
| | | */ |
| | | public static final String SYS_DEPARTS_CACHE = "sys:cache:depart:alldata"; |
| | | |
| | | |
| | | /** |
| | | * 全部部门ids缓存 |
| | | */ |
| | | public static final String SYS_DEPART_IDS_CACHE = "sys:cache:depart:allids"; |
| | | |
| | | |
| | | /** |
| | | * 测试缓存key |
| | | */ |
| | | public static final String TEST_DEMO_CACHE = "test:demo"; |
| | | |
| | | /** |
| | | * 字典信息缓存 |
| | | */ |
| | | public static final String SYS_DYNAMICDB_CACHE = "sys:cache:dbconnect:dynamic:"; |
| | | |
| | | /** |
| | | * gateway路由缓存 |
| | | */ |
| | | public static final String GATEWAY_ROUTES = "sys:cache:cloud:gateway_routes"; |
| | | |
| | | |
| | | /** |
| | | * gatewayAPI缓存 |
| | | */ |
| | | public static final String GATEWAY_APIS = "sys:cache:cloud:gateway_apis"; |
| | | |
| | | /** |
| | | * gateway路由 reload key |
| | | */ |
| | | public static final String ROUTE_JVM_RELOAD_TOPIC = "gateway_jvm_route_reload_topic"; |
| | | |
| | | /** |
| | | * TODO 冗余代码 待删除 |
| | | *插件商城排行榜 |
| | | */ |
| | | public static final String PLUGIN_MALL_RANKING = "pluginMall::rankingList"; |
| | | /** |
| | | * TODO 冗余代码 待删除 |
| | | *插件商城排行榜 |
| | | */ |
| | | public static final String PLUGIN_MALL_PAGE_LIST = "pluginMall::queryPageList"; |
| | | |
| | | |
| | | /** |
| | | * online列表页配置信息缓存key |
| | | */ |
| | | public static final String ONLINE_LIST = "sys:cache:online:list"; |
| | | |
| | | /** |
| | | * online表单页配置信息缓存key |
| | | */ |
| | | public static final String ONLINE_FORM = "sys:cache:online:form"; |
| | | |
| | | /** |
| | | * online报表 |
| | | */ |
| | | public static final String ONLINE_RP = "sys:cache:online:rp"; |
| | | |
| | | /** |
| | | * online图表 |
| | | */ |
| | | public static final String ONLINE_GRAPH = "sys:cache:online:graph"; |
| | | /** |
| | | * 拖拽页面信息缓存 |
| | | */ |
| | | public static final String DRAG_PAGE_CACHE = "drag:cache:param"; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.constant; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | /** |
| | | * @Description: 通用常量 |
| | | */ |
| | | public interface CommonConstant { |
| | | |
| | | BigDecimal BAD_VALUE = new BigDecimal("-2"); |
| | | |
| | | BigDecimal ZERO_VALUE = new BigDecimal("0"); |
| | | |
| | | /** |
| | | * 正常状态 |
| | | */ |
| | | public static final Integer STATUS_NORMAL = 0; |
| | | |
| | | /** |
| | | * 禁用状态 |
| | | */ |
| | | public static final Integer STATUS_DISABLE = -1; |
| | | |
| | | /** |
| | | * 删除标志 |
| | | */ |
| | | public static final Integer DEL_FLAG_1 = 1; |
| | | |
| | | /** |
| | | * 未删除 |
| | | */ |
| | | public static final Integer DEL_FLAG_0 = 0; |
| | | |
| | | /** |
| | | * 未提交 |
| | | */ |
| | | public static final Integer SUBMINT_STATUS_0 = 0; |
| | | |
| | | /** |
| | | * 系统日志类型: 登录 |
| | | */ |
| | | public static final int LOG_TYPE_1 = 1; |
| | | |
| | | /** |
| | | * 系统日志类型: 操作 |
| | | */ |
| | | public static final int LOG_TYPE_2 = 2; |
| | | |
| | | /** |
| | | * 操作日志类型: 查询 |
| | | */ |
| | | public static final int OPERATE_TYPE_1 = 1; |
| | | |
| | | /** |
| | | * 操作日志类型: 添加 |
| | | */ |
| | | public static final int OPERATE_TYPE_2 = 2; |
| | | |
| | | /** |
| | | * 操作日志类型: 更新 |
| | | */ |
| | | public static final int OPERATE_TYPE_3 = 3; |
| | | |
| | | /** |
| | | * 操作日志类型: 删除 |
| | | */ |
| | | public static final int OPERATE_TYPE_4 = 4; |
| | | |
| | | /** |
| | | * 操作日志类型: 倒入 |
| | | */ |
| | | public static final int OPERATE_TYPE_5 = 5; |
| | | |
| | | /** |
| | | * 操作日志类型: 导出 |
| | | */ |
| | | public static final int OPERATE_TYPE_6 = 6; |
| | | |
| | | /** |
| | | * 提交 |
| | | */ |
| | | public static final int SUBMIT_FLAG_1 = 1; |
| | | |
| | | /** |
| | | * 提交 |
| | | */ |
| | | public static final int SUBMIT_FLAG_0 = 0; |
| | | |
| | | /** |
| | | * 启用 |
| | | */ |
| | | public static final int IS_ENABLE = 1; |
| | | |
| | | /** |
| | | * 常量点类型 |
| | | */ |
| | | public static final String POINT_TYPE_NAME_CONSTANT = "CONSTANT"; |
| | | |
| | | |
| | | /** {@code 500 Server Error} (HTTP/1.0 - RFC 1945) */ |
| | | public static final Integer SC_INTERNAL_SERVER_ERROR_500 = 500; |
| | | /** {@code 200 OK} (HTTP/1.0 - RFC 1945) */ |
| | | public static final Integer SC_OK_200 = 200; |
| | | |
| | | /**访问权限认证未通过 510*/ |
| | | public static final Integer SC_JEECG_NO_AUTHZ=510; |
| | | |
| | | /** 登录用户Shiro权限缓存KEY前缀 */ |
| | | public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:"; |
| | | /** 登录用户Token令牌缓存KEY前缀 */ |
| | | public static final String PREFIX_USER_TOKEN = "prefix_user_token_"; |
| | | // /** Token缓存时间:3600秒即一小时 */ |
| | | // public static final int TOKEN_EXPIRE_TIME = 3600; |
| | | |
| | | /** 登录二维码 */ |
| | | public static final String LOGIN_QRCODE_PRE = "QRCODELOGIN:"; |
| | | public static final String LOGIN_QRCODE = "LQ:"; |
| | | /** 登录二维码token */ |
| | | public static final String LOGIN_QRCODE_TOKEN = "LQT:"; |
| | | |
| | | |
| | | /** |
| | | * 0:一级菜单 |
| | | */ |
| | | public static final Integer MENU_TYPE_0 = 0; |
| | | /** |
| | | * 1:子菜单 |
| | | */ |
| | | public static final Integer MENU_TYPE_1 = 1; |
| | | /** |
| | | * 2:按钮权限 |
| | | */ |
| | | public static final Integer MENU_TYPE_2 = 2; |
| | | |
| | | /**通告对象类型(USER:指定用户,ALL:全体用户)*/ |
| | | public static final String MSG_TYPE_UESR = "USER"; |
| | | public static final String MSG_TYPE_ALL = "ALL"; |
| | | |
| | | /**发布状态(0未发布,1已发布,2已撤销)*/ |
| | | public static final String NO_SEND = "0"; |
| | | public static final String HAS_SEND = "1"; |
| | | public static final String HAS_CANCLE = "2"; |
| | | |
| | | /**阅读状态(0未读,1已读)*/ |
| | | public static final String HAS_READ_FLAG = "1"; |
| | | public static final String NO_READ_FLAG = "0"; |
| | | |
| | | /**优先级(L低,M中,H高)*/ |
| | | public static final String PRIORITY_L = "L"; |
| | | public static final String PRIORITY_M = "M"; |
| | | public static final String PRIORITY_H = "H"; |
| | | |
| | | /** |
| | | * 短信模板方式 0 .登录模板、1.注册模板、2.忘记密码模板 |
| | | */ |
| | | public static final String SMS_TPL_TYPE_0 = "0"; |
| | | public static final String SMS_TPL_TYPE_1 = "1"; |
| | | public static final String SMS_TPL_TYPE_2 = "2"; |
| | | |
| | | /** |
| | | * 状态(0无效1有效) |
| | | */ |
| | | public static final String STATUS_0 = "0"; |
| | | public static final String STATUS_1 = "1"; |
| | | |
| | | /** |
| | | * 同步工作流引擎1同步0不同步 |
| | | */ |
| | | public static final Integer ACT_SYNC_1 = 1; |
| | | public static final Integer ACT_SYNC_0 = 0; |
| | | |
| | | /** |
| | | * 消息类型1:通知公告2:系统消息 |
| | | */ |
| | | public static final String MSG_CATEGORY_1 = "1"; |
| | | public static final String MSG_CATEGORY_2 = "2"; |
| | | |
| | | /** |
| | | * 是否配置菜单的数据权限 1是0否 |
| | | */ |
| | | public static final Integer RULE_FLAG_0 = 0; |
| | | public static final Integer RULE_FLAG_1 = 1; |
| | | |
| | | /** |
| | | * 是否用户已被冻结 1正常(解冻) 2冻结 |
| | | */ |
| | | public static final Integer USER_UNFREEZE = 1; |
| | | public static final Integer USER_FREEZE = 2; |
| | | |
| | | /**字典翻译文本后缀*/ |
| | | public static final String DICT_TEXT_SUFFIX = "_dictText"; |
| | | |
| | | /** |
| | | * 表单设计器主表类型 |
| | | */ |
| | | public static final Integer DESIGN_FORM_TYPE_MAIN = 1; |
| | | |
| | | /** |
| | | * 表单设计器子表表类型 |
| | | */ |
| | | public static final Integer DESIGN_FORM_TYPE_SUB = 2; |
| | | |
| | | /** |
| | | * 表单设计器URL授权通过 |
| | | */ |
| | | public static final Integer DESIGN_FORM_URL_STATUS_PASSED = 1; |
| | | |
| | | /** |
| | | * 表单设计器URL授权未通过 |
| | | */ |
| | | public static final Integer DESIGN_FORM_URL_STATUS_NOT_PASSED = 2; |
| | | |
| | | /** |
| | | * 表单设计器新增 Flag |
| | | */ |
| | | public static final String DESIGN_FORM_URL_TYPE_ADD = "add"; |
| | | /** |
| | | * 表单设计器修改 Flag |
| | | */ |
| | | public static final String DESIGN_FORM_URL_TYPE_EDIT = "edit"; |
| | | /** |
| | | * 表单设计器详情 Flag |
| | | */ |
| | | public static final String DESIGN_FORM_URL_TYPE_DETAIL = "detail"; |
| | | /** |
| | | * 表单设计器复用数据 Flag |
| | | */ |
| | | public static final String DESIGN_FORM_URL_TYPE_REUSE = "reuse"; |
| | | /** |
| | | * 表单设计器编辑 Flag (已弃用) |
| | | */ |
| | | public static final String DESIGN_FORM_URL_TYPE_VIEW = "view"; |
| | | |
| | | /** |
| | | * online参数值设置(是:Y, 否:N) |
| | | */ |
| | | public static final String ONLINE_PARAM_VAL_IS_TURE = "Y"; |
| | | public static final String ONLINE_PARAM_VAL_IS_FALSE = "N"; |
| | | |
| | | /** |
| | | * 文件上传类型(本地:local,Minio:minio,阿里云:alioss) |
| | | */ |
| | | public static final String UPLOAD_TYPE_LOCAL = "local"; |
| | | public static final String UPLOAD_TYPE_MINIO = "minio"; |
| | | public static final String UPLOAD_TYPE_OSS = "alioss"; |
| | | |
| | | /** |
| | | * 文档上传自定义桶名称 |
| | | */ |
| | | public static final String UPLOAD_CUSTOM_BUCKET = "eoafile"; |
| | | /** |
| | | * 文档上传自定义路径 |
| | | */ |
| | | public static final String UPLOAD_CUSTOM_PATH = "eoafile"; |
| | | /** |
| | | * 文件外链接有效天数 |
| | | */ |
| | | public static final Integer UPLOAD_EFFECTIVE_DAYS = 1; |
| | | |
| | | /** |
| | | * 员工身份 (1:普通员工 2:上级) |
| | | */ |
| | | public static final Integer USER_IDENTITY_1 = 1; |
| | | public static final Integer USER_IDENTITY_2 = 2; |
| | | |
| | | /** sys_user 表 username 唯一键索引 */ |
| | | public static final String SQL_INDEX_UNIQ_SYS_USER_USERNAME = "uniq_sys_user_username"; |
| | | /** sys_user 表 work_no 唯一键索引 */ |
| | | public static final String SQL_INDEX_UNIQ_SYS_USER_WORK_NO = "uniq_sys_user_work_no"; |
| | | /** sys_user 表 phone 唯一键索引 */ |
| | | public static final String SQL_INDEX_UNIQ_SYS_USER_PHONE = "uniq_sys_user_phone"; |
| | | /** 达梦数据库升提示。违反表[SYS_USER]唯一性约束 */ |
| | | public static final String SQL_INDEX_UNIQ_SYS_USER = "唯一性约束"; |
| | | |
| | | /** sys_user 表 email 唯一键索引 */ |
| | | public static final String SQL_INDEX_UNIQ_SYS_USER_EMAIL = "uniq_sys_user_email"; |
| | | /** sys_quartz_job 表 job_class_name 唯一键索引 */ |
| | | public static final String SQL_INDEX_UNIQ_JOB_CLASS_NAME = "uniq_job_class_name"; |
| | | /** sys_position 表 code 唯一键索引 */ |
| | | public static final String SQL_INDEX_UNIQ_CODE = "uniq_code"; |
| | | /** sys_role 表 code 唯一键索引 */ |
| | | public static final String SQL_INDEX_UNIQ_SYS_ROLE_CODE = "uniq_sys_role_role_code"; |
| | | /** sys_depart 表 code 唯一键索引 */ |
| | | public static final String SQL_INDEX_UNIQ_DEPART_ORG_CODE = "uniq_depart_org_code"; |
| | | /** sys_category 表 code 唯一键索引 */ |
| | | public static final String SQL_INDEX_UNIQ_CATEGORY_CODE = "idx_sc_code"; |
| | | /** |
| | | * 在线聊天 是否为默认分组 |
| | | */ |
| | | public static final String IM_DEFAULT_GROUP = "1"; |
| | | /** |
| | | * 在线聊天 图片文件保存路径 |
| | | */ |
| | | public static final String IM_UPLOAD_CUSTOM_PATH = "imfile"; |
| | | /** |
| | | * 在线聊天 用户状态 |
| | | */ |
| | | public static final String IM_STATUS_ONLINE = "online"; |
| | | |
| | | /** |
| | | * 在线聊天 SOCKET消息类型 |
| | | */ |
| | | public static final String IM_SOCKET_TYPE = "chatMessage"; |
| | | |
| | | /** |
| | | * 在线聊天 是否开启默认添加好友 1是 0否 |
| | | */ |
| | | public static final String IM_DEFAULT_ADD_FRIEND = "1"; |
| | | |
| | | /** |
| | | * 在线聊天 用户好友缓存前缀 |
| | | */ |
| | | public static final String IM_PREFIX_USER_FRIEND_CACHE = "sys:cache:im:im_prefix_user_friend_"; |
| | | |
| | | /** |
| | | * 考勤补卡业务状态 (1:同意 2:不同意) |
| | | */ |
| | | public static final String SIGN_PATCH_BIZ_STATUS_1 = "1"; |
| | | public static final String SIGN_PATCH_BIZ_STATUS_2 = "2"; |
| | | |
| | | /** |
| | | * 公文文档上传自定义路径 |
| | | */ |
| | | public static final String UPLOAD_CUSTOM_PATH_OFFICIAL = "officialdoc"; |
| | | /** |
| | | * 公文文档下载自定义路径 |
| | | */ |
| | | public static final String DOWNLOAD_CUSTOM_PATH_OFFICIAL = "officaldown"; |
| | | |
| | | /** |
| | | * WPS存储值类别(1 code文号 2 text(WPS模板还是公文发文模板)) |
| | | */ |
| | | public static final String WPS_TYPE_1="1"; |
| | | public static final String WPS_TYPE_2="2"; |
| | | |
| | | |
| | | public final static String X_ACCESS_TOKEN = "X-Access-Token"; |
| | | public final static String X_SIGN = "X-Sign"; |
| | | public final static String X_TIMESTAMP = "X-TIMESTAMP"; |
| | | public final static String TOKEN_IS_INVALID_MSG = "Token失效,请重新登录!"; |
| | | |
| | | /** |
| | | * 多租户 请求头 |
| | | */ |
| | | public final static String TENANT_ID = "tenant-id"; |
| | | |
| | | /** |
| | | * 微服务读取配置文件属性 服务地址 |
| | | */ |
| | | public final static String CLOUD_SERVER_KEY = "spring.cloud.nacos.discovery.server-addr"; |
| | | |
| | | /** |
| | | * 第三方登录 验证密码/创建用户 都需要设置一个操作码 防止被恶意调用 |
| | | */ |
| | | public final static String THIRD_LOGIN_CODE = "third_login_code"; |
| | | |
| | | /** |
| | | * 第三方APP同步方向:本地 --> 第三方APP |
| | | */ |
| | | String THIRD_SYNC_TO_APP = "SYNC_TO_APP"; |
| | | /** |
| | | * 第三方APP同步方向:第三方APP --> 本地 |
| | | */ |
| | | String THIRD_SYNC_TO_LOCAL = "SYNC_TO_LOCAL"; |
| | | |
| | | /** 系统通告消息状态:0=未发布 */ |
| | | String ANNOUNCEMENT_SEND_STATUS_0 = "0"; |
| | | /** 系统通告消息状态:1=已发布 */ |
| | | String ANNOUNCEMENT_SEND_STATUS_1 = "1"; |
| | | /** 系统通告消息状态:2=已撤销 */ |
| | | String ANNOUNCEMENT_SEND_STATUS_2 = "2"; |
| | | |
| | | /**ONLINE 报表权限用 从request中获取地址栏后的参数*/ |
| | | String ONL_REP_URL_PARAM_STR="onlRepUrlParamStr"; |
| | | |
| | | /**POST请求*/ |
| | | String HTTP_POST = "POST"; |
| | | |
| | | /**PUT请求*/ |
| | | String HTTP_PUT = "PUT"; |
| | | |
| | | /**PATCH请求*/ |
| | | String HTTP_PATCH = "PATCH"; |
| | | |
| | | /**未知的*/ |
| | | String UNKNOWN = "unknown"; |
| | | |
| | | /**字符串http*/ |
| | | String STR_HTTP = "http"; |
| | | |
| | | /**String 类型的空值*/ |
| | | String STRING_NULL = "null"; |
| | | |
| | | /**java.util.Date 包*/ |
| | | String JAVA_UTIL_DATE = "java.util.Date"; |
| | | |
| | | /**.do*/ |
| | | String SPOT_DO = ".do"; |
| | | |
| | | |
| | | /**前端vue版本标识*/ |
| | | String VERSION="X-Version"; |
| | | |
| | | /**前端vue版本*/ |
| | | String VERSION_VUE3="vue3"; |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * |
| | | * https://www.renren.io |
| | | * |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.common.constant; |
| | | |
| | | /** |
| | | * 常量 |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | */ |
| | | public interface Constant { |
| | | /** |
| | | * 成功 |
| | | */ |
| | | int SUCCESS = 1; |
| | | /** |
| | | * 失败 |
| | | */ |
| | | int FAIL = 0; |
| | | /** |
| | | * 菜单根节点标识 |
| | | */ |
| | | Long MENU_ROOT = 0L; |
| | | /** |
| | | * 部门根节点标识 |
| | | */ |
| | | Long DEPT_ROOT = 0L; |
| | | /** |
| | | * 升序 |
| | | */ |
| | | String ASC = "asc"; |
| | | /** |
| | | * 降序 |
| | | */ |
| | | String DESC = "desc"; |
| | | /** |
| | | * 创建时间字段名 |
| | | */ |
| | | String CREATE_DATE = "create_date"; |
| | | |
| | | String CREATE_TIME = "create_time"; |
| | | |
| | | /** |
| | | * 数据权限过滤 |
| | | */ |
| | | String SQL_FILTER = "sqlFilter"; |
| | | /** |
| | | * 当前页码 |
| | | */ |
| | | String PAGE = "page"; |
| | | /** |
| | | * 每页显示记录数 |
| | | */ |
| | | String LIMIT = "limit"; |
| | | /** |
| | | * 排序字段 |
| | | */ |
| | | String ORDER_FIELD = "orderField"; |
| | | /** |
| | | * 排序方式 |
| | | */ |
| | | String ORDER = "order"; |
| | | /** |
| | | * token header |
| | | */ |
| | | String TOKEN_HEADER = "authorization"; |
| | | |
| | | /** |
| | | * tenantCode |
| | | */ |
| | | String TENANT_CODE = "tenantCode"; |
| | | |
| | | /** |
| | | * tenantId |
| | | */ |
| | | String TENANT_ID = "tenantId"; |
| | | |
| | | /** |
| | | * tenantId |
| | | */ |
| | | String HEAD_TENANT_ID = "tenant-id"; |
| | | |
| | | /** |
| | | * 云存储配置KEY |
| | | */ |
| | | String CLOUD_STORAGE_CONFIG_KEY = "CLOUD_STORAGE_CONFIG_KEY"; |
| | | |
| | | Integer DEL_FLAG_0 = 0; |
| | | |
| | | enum EnableStatus { |
| | | DISABLED(0), |
| | | NORMAL(1); |
| | | |
| | | private int value; |
| | | |
| | | private EnableStatus(int value) { |
| | | this.value = value; |
| | | } |
| | | |
| | | public int getValue() { |
| | | return this.value; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 定时任务状态 |
| | | */ |
| | | enum ScheduleStatus { |
| | | /** |
| | | * 暂停 |
| | | */ |
| | | PAUSE(0), |
| | | /** |
| | | * 正常 |
| | | */ |
| | | NORMAL(1); |
| | | |
| | | private int value; |
| | | |
| | | ScheduleStatus(int value) { |
| | | this.value = value; |
| | | } |
| | | |
| | | public int getValue() { |
| | | return value; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 云服务商 |
| | | */ |
| | | enum CloudService { |
| | | /** |
| | | * 七牛云 |
| | | */ |
| | | QINIU(1), |
| | | /** |
| | | * 阿里云 |
| | | */ |
| | | ALIYUN(2), |
| | | /** |
| | | * 腾讯云 |
| | | */ |
| | | QCLOUD(3); |
| | | |
| | | private int value; |
| | | |
| | | CloudService(int value) { |
| | | this.value = value; |
| | | } |
| | | |
| | | public int getValue() { |
| | | return value; |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.constant; |
| | | |
| | | /** |
| | | * @Description: GlobalConstants |
| | | * @author: scott |
| | | * @date: 2020/01/01 16:01 |
| | | */ |
| | | public class GlobalConstants { |
| | | |
| | | /** |
| | | * 业务处理器beanName传递参数 |
| | | */ |
| | | public static final String HANDLER_NAME = "handlerName"; |
| | | |
| | | /** |
| | | * 路由刷新触发器 |
| | | */ |
| | | public static final String LODER_ROUDER_HANDLER = "loderRouderHandler"; |
| | | |
| | | /** |
| | | * API刷新触发器 |
| | | */ |
| | | public static final String LODER_API_HANDLER = "loderApiHandler"; |
| | | |
| | | /** |
| | | * redis消息通道名称 |
| | | */ |
| | | public static final String REDIS_TOPIC_NAME="redis_topic"; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.core; |
| | | |
| | | /** |
| | | * 可生成 Int 数组的接口 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface IntArrayValuable { |
| | | |
| | | /** |
| | | * @return int 数组 |
| | | */ |
| | | int[] array(); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.core; |
| | | |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Data; |
| | | import lombok.NoArgsConstructor; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | /** |
| | | * Key Value 的键值对 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Data |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | public class KeyValue<K, V> implements Serializable { |
| | | |
| | | private K key; |
| | | private V value; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.dto; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * @author PanZhibao |
| | | * @Description |
| | | * @createTime 2024年09月23日 |
| | | */ |
| | | @Data |
| | | public class TreeLabelDTO { |
| | | |
| | | private String value; |
| | | |
| | | private String label; |
| | | |
| | | private List<TreeLabelDTO> children; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.enums; |
| | | |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import com.iailab.framework.common.core.IntArrayValuable; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Getter; |
| | | |
| | | import java.util.Arrays; |
| | | |
| | | /** |
| | | * 通用状态枚举 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Getter |
| | | @AllArgsConstructor |
| | | public enum CommonStatusEnum implements IntArrayValuable { |
| | | |
| | | ENABLE(0, "开启"), |
| | | DISABLE(1, "关闭"); |
| | | |
| | | public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray(); |
| | | |
| | | /** |
| | | * 状态值 |
| | | */ |
| | | private final Integer status; |
| | | /** |
| | | * 状态名 |
| | | */ |
| | | private final String name; |
| | | |
| | | @Override |
| | | public int[] array() { |
| | | return ARRAYS; |
| | | } |
| | | |
| | | public static boolean isEnable(Integer status) { |
| | | return ObjUtil.equal(ENABLE.status, status); |
| | | } |
| | | |
| | | public static boolean isDisable(Integer status) { |
| | | return ObjUtil.equal(DISABLE.status, status); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.enums; |
| | | |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import com.iailab.framework.common.core.IntArrayValuable; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Getter; |
| | | |
| | | import java.util.Arrays; |
| | | |
| | | /** |
| | | * 时间间隔的枚举 |
| | | * |
| | | * @author dhb52 |
| | | */ |
| | | @Getter |
| | | @AllArgsConstructor |
| | | public enum DateIntervalEnum implements IntArrayValuable { |
| | | |
| | | DAY(1, "天"), |
| | | WEEK(2, "周"), |
| | | MONTH(3, "月"), |
| | | QUARTER(4, "季度"), |
| | | YEAR(5, "年") |
| | | ; |
| | | |
| | | public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DateIntervalEnum::getInterval).toArray(); |
| | | |
| | | /** |
| | | * 类型 |
| | | */ |
| | | private final Integer interval; |
| | | /** |
| | | * 名称 |
| | | */ |
| | | private final String name; |
| | | |
| | | @Override |
| | | public int[] array() { |
| | | return ARRAYS; |
| | | } |
| | | |
| | | public static DateIntervalEnum valueOf(Integer interval) { |
| | | return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.enums; |
| | | |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Getter; |
| | | |
| | | /** |
| | | * 文档地址 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Getter |
| | | @AllArgsConstructor |
| | | public enum DocumentEnum { |
| | | |
| | | REDIS_INSTALL("https://iailab.cn", "Redis 安装文档"), |
| | | TENANT("https://iailab.cn", "SaaS 多租户文档"); |
| | | |
| | | private final String url; |
| | | private final String memo; |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * |
| | | * https://www.renren.io |
| | | * |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.common.enums; |
| | | |
| | | /** |
| | | * 错误编码,由5位数字组成,前2位为模块编码,后3位为业务编码 |
| | | * <p> |
| | | * 如:10001(10代表系统模块,001代表业务代码) |
| | | * </p> |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | * @since 1.0.0 |
| | | */ |
| | | public interface ErrorCode { |
| | | int INTERNAL_SERVER_ERROR = 500; |
| | | int UNAUTHORIZED = 401; |
| | | |
| | | int NOT_NULL = 10001; |
| | | int DB_RECORD_EXISTS = 10002; |
| | | int PARAMS_GET_ERROR = 10003; |
| | | int ACCOUNT_PASSWORD_ERROR = 10004; |
| | | int ACCOUNT_DISABLE = 10005; |
| | | int IDENTIFIER_NOT_NULL = 10006; |
| | | int CAPTCHA_ERROR = 10007; |
| | | int SUB_MENU_EXIST = 10008; |
| | | int PASSWORD_ERROR = 10009; |
| | | int SUPERIOR_DEPT_ERROR = 10011; |
| | | int SUPERIOR_MENU_ERROR = 10012; |
| | | int DATA_SCOPE_PARAMS_ERROR = 10013; |
| | | int DEPT_SUB_DELETE_ERROR = 10014; |
| | | int DEPT_USER_DELETE_ERROR = 10015; |
| | | int UPLOAD_FILE_EMPTY = 10019; |
| | | int TOKEN_NOT_EMPTY = 10020; |
| | | int TOKEN_INVALID = 10021; |
| | | int ACCOUNT_LOCK = 10022; |
| | | int OSS_UPLOAD_FILE_ERROR = 10024; |
| | | int REDIS_ERROR = 10027; |
| | | int JOB_ERROR = 10028; |
| | | int INVALID_SYMBOL = 10029; |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.enums; |
| | | |
| | | /** |
| | | * RPC 相关的枚举 |
| | | * |
| | | * 虽然放在 iailab-common-rpc 会相对合适,但是每个 API 模块需要使用到,所以暂时只好放在此处 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class RpcConstants { |
| | | |
| | | /** |
| | | * RPC API 的前缀 |
| | | */ |
| | | public static final String RPC_API_PREFIX = "/rpc-api"; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.enums; |
| | | |
| | | import com.iailab.framework.common.core.IntArrayValuable; |
| | | import lombok.Getter; |
| | | import lombok.RequiredArgsConstructor; |
| | | |
| | | import java.util.Arrays; |
| | | |
| | | /** |
| | | * 终端的枚举 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | @Getter |
| | | public enum TerminalEnum implements IntArrayValuable { |
| | | |
| | | UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它 |
| | | WECHAT_MINI_PROGRAM(10, "微信小程序"), |
| | | WECHAT_WAP(11, "微信公众号"), |
| | | H5(20, "H5 网页"), |
| | | APP(31, "手机 App"), |
| | | ; |
| | | |
| | | public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TerminalEnum::getTerminal).toArray(); |
| | | |
| | | /** |
| | | * 终端 |
| | | */ |
| | | private final Integer terminal; |
| | | /** |
| | | * 终端名 |
| | | */ |
| | | private final String name; |
| | | |
| | | @Override |
| | | public int[] array() { |
| | | return ARRAYS; |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.enums; |
| | | |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import com.iailab.framework.common.core.IntArrayValuable; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Getter; |
| | | |
| | | import java.util.Arrays; |
| | | |
| | | /** |
| | | * 全局用户类型枚举 |
| | | */ |
| | | @AllArgsConstructor |
| | | @Getter |
| | | public enum UserTypeEnum implements IntArrayValuable { |
| | | |
| | | MEMBER(1, "会员"), // 面向 c 端,普通用户 |
| | | ADMIN(2, "管理员"); // 面向 b 端,管理后台 |
| | | |
| | | public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray(); |
| | | |
| | | /** |
| | | * 类型 |
| | | */ |
| | | private final Integer value; |
| | | /** |
| | | * 类型名 |
| | | */ |
| | | private final String name; |
| | | |
| | | public static UserTypeEnum valueOf(Integer value) { |
| | | return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values()); |
| | | } |
| | | |
| | | @Override |
| | | public int[] array() { |
| | | return ARRAYS; |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.enums; |
| | | |
| | | /** |
| | | * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 |
| | | * |
| | | * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enum 包下 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface WebFilterOrderEnum { |
| | | |
| | | int CORS_FILTER = Integer.MIN_VALUE; |
| | | |
| | | int TRACE_FILTER = CORS_FILTER + 1; |
| | | |
| | | int ENV_TAG_FILTER = TRACE_FILTER + 1; |
| | | |
| | | int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; |
| | | |
| | | // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 |
| | | |
| | | int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 |
| | | |
| | | int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面 |
| | | |
| | | int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面 |
| | | |
| | | // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类 |
| | | |
| | | int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面 |
| | | |
| | | int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面 |
| | | |
| | | int DEMO_FILTER = Integer.MAX_VALUE; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.exception; |
| | | |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import com.iailab.framework.common.exception.enums.ServiceErrorCodeRange; |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * 错误码对象 |
| | | * |
| | | * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants} |
| | | * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} |
| | | * |
| | | * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备 |
| | | */ |
| | | @Data |
| | | public class ErrorCode { |
| | | |
| | | /** |
| | | * 错误码 |
| | | */ |
| | | private final Integer code; |
| | | /** |
| | | * 错误提示 |
| | | */ |
| | | private final String msg; |
| | | |
| | | public ErrorCode(Integer code, String message) { |
| | | this.code = code; |
| | | this.msg = message; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * Copyright (c) 2018 人人开源 All rights reserved. |
| | | * |
| | | * https://www.renren.io |
| | | * |
| | | * 版权所有,侵权必究! |
| | | */ |
| | | |
| | | package com.iailab.framework.common.exception; |
| | | |
| | | import java.io.IOException; |
| | | import java.io.PrintWriter; |
| | | import java.io.StringWriter; |
| | | |
| | | /** |
| | | * Exception工具类 |
| | | * |
| | | * @author Mark sunlightcs@gmail.com |
| | | */ |
| | | public class ExceptionUtils { |
| | | |
| | | /** |
| | | * 获取异常信息 |
| | | * @param ex 异常 |
| | | * @return 返回异常信息 |
| | | */ |
| | | public static String getErrorStackTrace(Exception ex){ |
| | | StringWriter sw = null; |
| | | PrintWriter pw = null; |
| | | try { |
| | | sw = new StringWriter(); |
| | | pw = new PrintWriter(sw, true); |
| | | ex.printStackTrace(pw); |
| | | }finally { |
| | | try { |
| | | if(pw != null) { |
| | | pw.close(); |
| | | } |
| | | } catch (Exception e) { |
| | | |
| | | } |
| | | try { |
| | | if(sw != null) { |
| | | sw.close(); |
| | | } |
| | | } catch (IOException e) { |
| | | |
| | | } |
| | | } |
| | | |
| | | return sw.toString(); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.exception; |
| | | |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | |
| | | /** |
| | | * 服务器异常 Exception |
| | | */ |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | public final class ServerException extends RuntimeException { |
| | | |
| | | /** |
| | | * 全局错误码 |
| | | * |
| | | * @see GlobalErrorCodeConstants |
| | | */ |
| | | private Integer code; |
| | | /** |
| | | * 错误提示 |
| | | */ |
| | | private String message; |
| | | |
| | | /** |
| | | * 空构造方法,避免反序列化问题 |
| | | */ |
| | | public ServerException() { |
| | | } |
| | | |
| | | public ServerException(ErrorCode errorCode) { |
| | | this.code = errorCode.getCode(); |
| | | this.message = errorCode.getMsg(); |
| | | } |
| | | |
| | | public ServerException(Integer code, String message) { |
| | | this.code = code; |
| | | this.message = message; |
| | | } |
| | | |
| | | public Integer getCode() { |
| | | return code; |
| | | } |
| | | |
| | | public ServerException setCode(Integer code) { |
| | | this.code = code; |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public String getMessage() { |
| | | return message; |
| | | } |
| | | |
| | | public ServerException setMessage(String message) { |
| | | this.message = message; |
| | | return this; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.exception; |
| | | |
| | | import com.iailab.framework.common.exception.enums.ServiceErrorCodeRange; |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | |
| | | /** |
| | | * 业务逻辑异常 Exception |
| | | */ |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | public final class ServiceException extends RuntimeException { |
| | | |
| | | /** |
| | | * 业务错误码 |
| | | * |
| | | * @see ServiceErrorCodeRange |
| | | */ |
| | | private Integer code; |
| | | /** |
| | | * 错误提示 |
| | | */ |
| | | private String message; |
| | | |
| | | /** |
| | | * 空构造方法,避免反序列化问题 |
| | | */ |
| | | public ServiceException() { |
| | | } |
| | | |
| | | public ServiceException(ErrorCode errorCode) { |
| | | this.code = errorCode.getCode(); |
| | | this.message = errorCode.getMsg(); |
| | | } |
| | | |
| | | public ServiceException(Integer code, String message) { |
| | | this.code = code; |
| | | this.message = message; |
| | | } |
| | | |
| | | public Integer getCode() { |
| | | return code; |
| | | } |
| | | |
| | | public ServiceException setCode(Integer code) { |
| | | this.code = code; |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public String getMessage() { |
| | | return message; |
| | | } |
| | | |
| | | public ServiceException setMessage(String message) { |
| | | this.message = message; |
| | | return this; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.exception.enums; |
| | | |
| | | import com.iailab.framework.common.exception.ErrorCode; |
| | | |
| | | /** |
| | | * 全局错误码枚举 |
| | | * 0-999 系统异常编码保留 |
| | | * |
| | | * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status |
| | | * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 |
| | | * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface GlobalErrorCodeConstants { |
| | | |
| | | ErrorCode SUCCESS = new ErrorCode(0, "成功"); |
| | | |
| | | // ========== 客户端错误段 ========== |
| | | |
| | | ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确"); |
| | | ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录"); |
| | | ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限"); |
| | | ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到"); |
| | | ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); |
| | | ErrorCode DATA_REPETITION = new ErrorCode(406, "数据库存在重复数据"); |
| | | ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许 |
| | | ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试"); |
| | | |
| | | // ========== 服务端错误段 ========== |
| | | |
| | | ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); |
| | | ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启"); |
| | | ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "错误的配置项"); |
| | | |
| | | // ========== 自定义错误段 ========== |
| | | ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求 |
| | | ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作"); |
| | | |
| | | ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.exception.enums; |
| | | |
| | | /** |
| | | * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用 |
| | | * |
| | | * 一共 10 位,分成四段 |
| | | * |
| | | * 第一段,1 位,类型 |
| | | * 1 - 业务级别异常 |
| | | * x - 预留 |
| | | * 第二段,3 位,系统类型 |
| | | * 001 - 用户系统 |
| | | * 002 - 商品系统 |
| | | * 003 - 订单系统 |
| | | * 004 - 支付系统 |
| | | * 005 - 优惠劵系统 |
| | | * ... - ... |
| | | * 第三段,3 位,模块 |
| | | * 不限制规则。 |
| | | * 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子: |
| | | * 001 - OAuth2 模块 |
| | | * 002 - User 模块 |
| | | * 003 - MobileCode 模块 |
| | | * 第四段,3 位,错误码 |
| | | * 不限制规则。 |
| | | * 一般建议,每个模块自增。 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class ServiceErrorCodeRange { |
| | | |
| | | // 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000) |
| | | // 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000) |
| | | // 模块 report 错误码区间 [1-003-000-000 ~ 1-004-000-000) |
| | | // 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000) |
| | | // 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000) |
| | | // 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000) |
| | | // 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000) |
| | | |
| | | // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000) |
| | | // 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000) |
| | | // 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000) |
| | | |
| | | // 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000) |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.exception.util; |
| | | |
| | | import com.iailab.framework.common.exception.ErrorCode; |
| | | import com.iailab.framework.common.exception.ServiceException; |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import com.google.common.annotations.VisibleForTesting; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | /** |
| | | * {@link ServiceException} 工具类 |
| | | * |
| | | * 目的在于,格式化异常信息提示。 |
| | | * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 |
| | | * |
| | | */ |
| | | @Slf4j |
| | | public class ServiceExceptionUtil { |
| | | |
| | | // ========== 和 ServiceException 的集成 ========== |
| | | |
| | | public static ServiceException exception(ErrorCode errorCode) { |
| | | return exception0(errorCode.getCode(), errorCode.getMsg()); |
| | | } |
| | | |
| | | public static ServiceException exception(ErrorCode errorCode, Object... params) { |
| | | return exception0(errorCode.getCode(), errorCode.getMsg(), params); |
| | | } |
| | | |
| | | public static ServiceException exception0(Integer code, String messagePattern, Object... params) { |
| | | String message = doFormat(code, messagePattern, params); |
| | | return new ServiceException(code, message); |
| | | } |
| | | |
| | | public static ServiceException invalidParamException(String messagePattern, Object... params) { |
| | | return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params); |
| | | } |
| | | |
| | | // ========== 格式化方法 ========== |
| | | |
| | | /** |
| | | * 将错误编号对应的消息使用 params 进行格式化。 |
| | | * |
| | | * @param code 错误编号 |
| | | * @param messagePattern 消息模版 |
| | | * @param params 参数 |
| | | * @return 格式化后的提示 |
| | | */ |
| | | @VisibleForTesting |
| | | public static String doFormat(int code, String messagePattern, Object... params) { |
| | | StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); |
| | | int i = 0; |
| | | int j; |
| | | int l; |
| | | for (l = 0; l < params.length; l++) { |
| | | j = messagePattern.indexOf("{}", i); |
| | | if (j == -1) { |
| | | log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); |
| | | if (i == 0) { |
| | | return messagePattern; |
| | | } else { |
| | | sbuf.append(messagePattern.substring(i)); |
| | | return sbuf.toString(); |
| | | } |
| | | } else { |
| | | sbuf.append(messagePattern, i, j); |
| | | sbuf.append(params[l]); |
| | | i = j + 2; |
| | | } |
| | | } |
| | | if (messagePattern.indexOf("{}", i) != -1) { |
| | | log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); |
| | | } |
| | | sbuf.append(messagePattern.substring(i)); |
| | | return sbuf.toString(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 基础的通用类,和框架无关 |
| | | * |
| | | * 例如说,CommonResult 为通用返回 |
| | | */ |
| | | package com.iailab.framework.common; |
对比新文件 |
| | |
| | | package com.iailab.framework.common.pojo; |
| | | |
| | | import cn.hutool.core.lang.Assert; |
| | | import com.iailab.framework.common.exception.ErrorCode; |
| | | import com.iailab.framework.common.exception.ServiceException; |
| | | import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; |
| | | import com.fasterxml.jackson.annotation.JsonIgnore; |
| | | import com.iailab.framework.common.exception.util.ServiceExceptionUtil; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Objects; |
| | | |
| | | /** |
| | | * 通用返回 |
| | | * |
| | | * @param <T> 数据泛型 |
| | | */ |
| | | @Data |
| | | public class CommonResult<T> implements Serializable { |
| | | |
| | | /** |
| | | * 错误码 |
| | | * |
| | | * @see ErrorCode#getCode() |
| | | */ |
| | | private Integer code; |
| | | /** |
| | | * 返回数据 |
| | | */ |
| | | private T data; |
| | | /** |
| | | * 错误提示,用户可阅读 |
| | | * |
| | | * @see ErrorCode#getMsg() () |
| | | */ |
| | | private String msg; |
| | | |
| | | /** |
| | | * 将传入的 result 对象,转换成另外一个泛型结果的对象 |
| | | * |
| | | * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 |
| | | * |
| | | * @param result 传入的 result 对象 |
| | | * @param <T> 返回的泛型 |
| | | * @return 新的 CommonResult 对象 |
| | | */ |
| | | public static <T> CommonResult<T> error(CommonResult<?> result) { |
| | | return error(result.getCode(), result.getMsg()); |
| | | } |
| | | |
| | | public static <T> CommonResult<T> error(Integer code, String message) { |
| | | cn.hutool.core.lang.Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), code, "code 必须是错误的!"); |
| | | CommonResult<T> result = new CommonResult<>(); |
| | | result.code = code; |
| | | result.msg = message; |
| | | return result; |
| | | } |
| | | |
| | | public static <T> CommonResult<T> error(ErrorCode errorCode, Object... params) { |
| | | Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), errorCode.getCode(), "code 必须是错误的!"); |
| | | CommonResult<T> result = new CommonResult<>(); |
| | | result.code = errorCode.getCode(); |
| | | result.msg = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), params); |
| | | return result; |
| | | } |
| | | |
| | | public static <T> CommonResult<T> error(ErrorCode errorCode) { |
| | | return error(errorCode.getCode(), errorCode.getMsg()); |
| | | } |
| | | |
| | | public static <T> CommonResult<T> success(T data) { |
| | | CommonResult<T> result = new CommonResult<>(); |
| | | result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); |
| | | result.data = data; |
| | | result.msg = ""; |
| | | return result; |
| | | } |
| | | |
| | | public static CommonResult<String> success() { |
| | | CommonResult<String> result = new CommonResult<>(); |
| | | result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); |
| | | result.msg = "success"; |
| | | return result; |
| | | } |
| | | |
| | | public static boolean isSuccess(Integer code) { |
| | | return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode()); |
| | | } |
| | | |
| | | @JsonIgnore // 避免 jackson 序列化 |
| | | public boolean isSuccess() { |
| | | return isSuccess(code); |
| | | } |
| | | |
| | | @JsonIgnore // 避免 jackson 序列化 |
| | | public boolean isError() { |
| | | return !isSuccess(); |
| | | } |
| | | |
| | | // ========= 和 Exception 异常体系集成 ========= |
| | | |
| | | /** |
| | | * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 |
| | | */ |
| | | public void checkError() throws ServiceException { |
| | | if (isSuccess()) { |
| | | return; |
| | | } |
| | | // 业务异常 |
| | | throw new ServiceException(code, msg); |
| | | } |
| | | |
| | | /** |
| | | * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 |
| | | * 如果没有,则返回 {@link #data} 数据 |
| | | */ |
| | | @JsonIgnore // 避免 jackson 序列化 |
| | | public T getCheckedData() { |
| | | checkError(); |
| | | return data; |
| | | } |
| | | |
| | | public static <T> CommonResult<T> error(ServiceException serviceException) { |
| | | return error(serviceException.getCode(), serviceException.getMessage()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.pojo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | |
| | | import javax.validation.constraints.Min; |
| | | import javax.validation.constraints.Max; |
| | | import javax.validation.constraints.NotNull; |
| | | import java.io.Serializable; |
| | | |
| | | @Schema(description="分页参数") |
| | | @Data |
| | | public class PageParam implements Serializable { |
| | | |
| | | private static final Integer PAGE_NO = 1; |
| | | private static final Integer PAGE_SIZE = 10; |
| | | |
| | | /** |
| | | * 每页条数 - 不分页 |
| | | * |
| | | * 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。 |
| | | */ |
| | | public static final Integer PAGE_SIZE_NONE = -1; |
| | | |
| | | @Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1") |
| | | @NotNull(message = "页码不能为空") |
| | | @Min(value = 1, message = "页码最小值为 1") |
| | | private Integer pageNo = PAGE_NO; |
| | | |
| | | @Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") |
| | | @NotNull(message = "每页条数不能为空") |
| | | @Min(value = 1, message = "每页条数最小值为 1") |
| | | @Max(value = 100, message = "每页条数最大值为 100") |
| | | private Integer pageSize = PAGE_SIZE; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.pojo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | @Schema(description = "分页结果") |
| | | @Data |
| | | public final class PageResult<T> implements Serializable { |
| | | |
| | | @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED) |
| | | private List<T> list; |
| | | |
| | | @Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED) |
| | | private Long total; |
| | | |
| | | public PageResult() { |
| | | } |
| | | |
| | | public PageResult(List<T> list, Long total) { |
| | | this.list = list; |
| | | this.total = total; |
| | | } |
| | | |
| | | public PageResult(Long total) { |
| | | this.list = new ArrayList<>(); |
| | | this.total = total; |
| | | } |
| | | |
| | | public static <T> PageResult<T> empty() { |
| | | return new PageResult<>(0L); |
| | | } |
| | | |
| | | public static <T> PageResult<T> empty(Long total) { |
| | | return new PageResult<>(total); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.pojo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | import lombok.ToString; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Schema(description = "可排序的分页参数") |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | @ToString(callSuper = true) |
| | | public class SortablePageParam extends PageParam { |
| | | |
| | | @Schema(description = "排序字段") |
| | | private List<SortingField> sortingFields; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.pojo; |
| | | |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Data; |
| | | import lombok.NoArgsConstructor; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | /** |
| | | * 排序字段 DTO |
| | | * |
| | | * 类名加了 ing 的原因是,避免和 ES SortField 重名。 |
| | | */ |
| | | @Data |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | public class SortingField implements Serializable { |
| | | |
| | | /** |
| | | * 顺序 - 升序 |
| | | */ |
| | | public static final String ORDER_ASC = "asc"; |
| | | /** |
| | | * 顺序 - 降序 |
| | | */ |
| | | public static final String ORDER_DESC = "desc"; |
| | | |
| | | /** |
| | | * 字段 |
| | | */ |
| | | private String field; |
| | | /** |
| | | * 顺序 |
| | | */ |
| | | private String order; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.util.cache; |
| | | |
| | | import com.google.common.cache.CacheBuilder; |
| | | import com.google.common.cache.CacheLoader; |
| | | import com.google.common.cache.LoadingCache; |
| | | |
| | | import java.time.Duration; |
| | | import java.util.concurrent.Executors; |
| | | |
| | | /** |
| | | * Cache 工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class CacheUtils { |
| | | |
| | | /** |
| | | * 构建异步刷新的 LoadingCache 对象 |
| | | * |
| | | * 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法 |
| | | * |
| | | * 或者简单理解: |
| | | * 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法 |
| | | * 2、和“全局”、“系统”相关的,使用当前缓存方法 |
| | | * |
| | | * @param duration 过期时间 |
| | | * @param loader CacheLoader 对象 |
| | | * @return LoadingCache 对象 |
| | | */ |
| | | public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) { |
| | | return CacheBuilder.newBuilder() |
| | | // 只阻塞当前数据加载线程,其他线程返回旧值 |
| | | .refreshAfterWrite(duration) |
| | | // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程 |
| | | .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO iailab:可能要思考下,未来要不要做成可配置 |
| | | } |
| | | |
| | | /** |
| | | * 构建同步刷新的 LoadingCache 对象 |
| | | * |
| | | * @param duration 过期时间 |
| | | * @param loader CacheLoader 对象 |
| | | * @return LoadingCache 对象 |
| | | */ |
| | | public static <K, V> LoadingCache<K, V> buildCache(Duration duration, CacheLoader<K, V> loader) { |
| | | return CacheBuilder.newBuilder().refreshAfterWrite(duration).build(loader); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.util.collection; |
| | | |
| | | import cn.hutool.core.collection.CollectionUtil; |
| | | import cn.hutool.core.collection.IterUtil; |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | |
| | | import java.util.Collection; |
| | | import java.util.function.Consumer; |
| | | import java.util.function.Function; |
| | | |
| | | import static com.iailab.framework.common.util.collection.CollectionUtils.convertList; |
| | | |
| | | /** |
| | | * Array 工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class ArrayUtils { |
| | | |
| | | /** |
| | | * 将 object 和 newElements 合并成一个数组 |
| | | * |
| | | * @param object 对象 |
| | | * @param newElements 数组 |
| | | * @param <T> 泛型 |
| | | * @return 结果数组 |
| | | */ |
| | | @SafeVarargs |
| | | public static <T> Consumer<T>[] append(Consumer<T> object, Consumer<T>... newElements) { |
| | | if (object == null) { |
| | | return newElements; |
| | | } |
| | | Consumer<T>[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length); |
| | | result[0] = object; |
| | | System.arraycopy(newElements, 0, result, 1, newElements.length); |
| | | return result; |
| | | } |
| | | |
| | | public static <T, V> V[] toArray(Collection<T> from, Function<T, V> mapper) { |
| | | return toArray(convertList(from, mapper)); |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | public static <T> T[] toArray(Collection<T> from) { |
| | | if (CollectionUtil.isEmpty(from)) { |
| | | return (T[]) (new Object[0]); |
| | | } |
| | | return ArrayUtil.toArray(from, (Class<T>) IterUtil.getElementType(from.iterator())); |
| | | } |
| | | |
| | | public static <T> T get(T[] array, int index) { |
| | | if (null == array || index >= array.length) { |
| | | return null; |
| | | } |
| | | return array[index]; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.util.collection; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.collection.CollectionUtil; |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import com.google.common.collect.ImmutableMap; |
| | | import com.iailab.framework.common.pojo.PageResult; |
| | | |
| | | import java.util.*; |
| | | import java.util.function.*; |
| | | import java.util.stream.Collectors; |
| | | import java.util.stream.Stream; |
| | | |
| | | import static java.util.Arrays.asList; |
| | | |
| | | /** |
| | | * Collection 工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class CollectionUtils { |
| | | |
| | | public static boolean containsAny(Object source, Object... targets) { |
| | | return asList(targets).contains(source); |
| | | } |
| | | |
| | | public static boolean isAnyEmpty(Collection<?>... collections) { |
| | | return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); |
| | | } |
| | | |
| | | public static <T> boolean anyMatch(Collection<T> from, Predicate<T> predicate) { |
| | | return from.stream().anyMatch(predicate); |
| | | } |
| | | |
| | | public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return from.stream().filter(predicate).collect(Collectors.toList()); |
| | | } |
| | | |
| | | public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return distinct(from, keyMapper, (t1, t2) -> t1); |
| | | } |
| | | |
| | | public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper, BinaryOperator<T> cover) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); |
| | | } |
| | | |
| | | public static <T, U> List<U> convertList(T[] from, Function<T, U> func) { |
| | | if (ArrayUtil.isEmpty(from)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return convertList(Arrays.asList(from), func); |
| | | } |
| | | |
| | | public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList()); |
| | | } |
| | | |
| | | public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func, Predicate<T> filter) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList()); |
| | | } |
| | | |
| | | public static <T, U> PageResult<U> convertPage(PageResult<T> from, Function<T, U> func) { |
| | | if (ArrayUtil.isEmpty(from)) { |
| | | return new PageResult<>(from.getTotal()); |
| | | } |
| | | return new PageResult<>(convertList(from.getList(), func), from.getTotal()); |
| | | } |
| | | |
| | | public static <T, U> List<U> convertListByFlatMap(Collection<T> from, |
| | | Function<T, ? extends Stream<? extends U>> func) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); |
| | | } |
| | | |
| | | public static <T, U, R> List<R> convertListByFlatMap(Collection<T> from, |
| | | Function<? super T, ? extends U> mapper, |
| | | Function<U, ? extends Stream<? extends R>> func) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); |
| | | } |
| | | |
| | | public static <K, V> List<V> mergeValuesFromMap(Map<K, List<V>> map) { |
| | | return map.values() |
| | | .stream() |
| | | .flatMap(List::stream) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | | public static <T> Set<T> convertSet(Collection<T> from) { |
| | | return convertSet(from, v -> v); |
| | | } |
| | | |
| | | public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashSet<>(); |
| | | } |
| | | return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | } |
| | | |
| | | public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashSet<>(); |
| | | } |
| | | return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | } |
| | | |
| | | public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashMap<>(); |
| | | } |
| | | return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v)); |
| | | } |
| | | |
| | | public static <T, U> Set<U> convertSetByFlatMap(Collection<T> from, |
| | | Function<T, ? extends Stream<? extends U>> func) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashSet<>(); |
| | | } |
| | | return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | } |
| | | |
| | | public static <T, U, R> Set<R> convertSetByFlatMap(Collection<T> from, |
| | | Function<? super T, ? extends U> mapper, |
| | | Function<U, ? extends Stream<? extends R>> func) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashSet<>(); |
| | | } |
| | | return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | } |
| | | |
| | | public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashMap<>(); |
| | | } |
| | | return convertMap(from, keyFunc, Function.identity()); |
| | | } |
| | | |
| | | public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc, Supplier<? extends Map<K, T>> supplier) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return supplier.get(); |
| | | } |
| | | return convertMap(from, keyFunc, Function.identity(), supplier); |
| | | } |
| | | |
| | | public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashMap<>(); |
| | | } |
| | | return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); |
| | | } |
| | | |
| | | public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashMap<>(); |
| | | } |
| | | return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); |
| | | } |
| | | |
| | | public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, Supplier<? extends Map<K, V>> supplier) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return supplier.get(); |
| | | } |
| | | return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); |
| | | } |
| | | |
| | | public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction, Supplier<? extends Map<K, V>> supplier) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashMap<>(); |
| | | } |
| | | return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); |
| | | } |
| | | |
| | | public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashMap<>(); |
| | | } |
| | | return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); |
| | | } |
| | | |
| | | public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashMap<>(); |
| | | } |
| | | return from.stream() |
| | | .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); |
| | | } |
| | | |
| | | // 暂时没想好名字,先以 2 结尾噶 |
| | | public static <T, K, V> Map<K, Set<V>> convertMultiMap2(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return new HashMap<>(); |
| | | } |
| | | return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); |
| | | } |
| | | |
| | | public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | ImmutableMap.Builder<K, T> builder = ImmutableMap.builder(); |
| | | from.forEach(item -> builder.put(keyFunc.apply(item), item)); |
| | | return builder.build(); |
| | | } |
| | | |
| | | /** |
| | | * 对比老、新两个列表,找出新增、修改、删除的数据 |
| | | * |
| | | * @param oldList 老列表 |
| | | * @param newList 新列表 |
| | | * @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同 |
| | | * 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据 |
| | | * @return [新增列表、修改列表、删除列表] |
| | | */ |
| | | public static <T> List<List<T>> diffList(Collection<T> oldList, Collection<T> newList, |
| | | BiFunction<T, T, Boolean> sameFunc) { |
| | | List<T> createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除 |
| | | List<T> updateList = new ArrayList<>(); |
| | | List<T> deleteList = new ArrayList<>(); |
| | | |
| | | // 通过以 oldList 为主遍历,找出 updateList 和 deleteList |
| | | for (T oldObj : oldList) { |
| | | // 1. 寻找是否有匹配的 |
| | | T foundObj = null; |
| | | for (Iterator<T> iterator = createList.iterator(); iterator.hasNext(); ) { |
| | | T newObj = iterator.next(); |
| | | // 1.1 不匹配,则直接跳过 |
| | | if (!sameFunc.apply(oldObj, newObj)) { |
| | | continue; |
| | | } |
| | | // 1.2 匹配,则移除,并结束寻找 |
| | | iterator.remove(); |
| | | foundObj = newObj; |
| | | break; |
| | | } |
| | | // 2. 匹配添加到 updateList;不匹配则添加到 deleteList 中 |
| | | if (foundObj != null) { |
| | | updateList.add(foundObj); |
| | | } else { |
| | | deleteList.add(oldObj); |
| | | } |
| | | } |
| | | return asList(createList, updateList, deleteList); |
| | | } |
| | | |
| | | public static boolean containsAny(Collection<?> source, Collection<?> candidates) { |
| | | return org.springframework.util.CollectionUtils.containsAny(source, candidates); |
| | | } |
| | | |
| | | public static <T> T getFirst(List<T> from) { |
| | | return !CollectionUtil.isEmpty(from) ? from.get(0) : null; |
| | | } |
| | | |
| | | public static <T> T findFirst(Collection<T> from, Predicate<T> predicate) { |
| | | return findFirst(from, predicate, Function.identity()); |
| | | } |
| | | |
| | | public static <T, U> U findFirst(Collection<T> from, Predicate<T> predicate, Function<T, U> func) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return null; |
| | | } |
| | | return from.stream().filter(predicate).findFirst().map(func).orElse(null); |
| | | } |
| | | |
| | | public static <T, V extends Comparable<? super V>> V getMaxValue(Collection<T> from, Function<T, V> valueFunc) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return null; |
| | | } |
| | | assert !from.isEmpty(); // 断言,避免告警 |
| | | T t = from.stream().max(Comparator.comparing(valueFunc)).get(); |
| | | return valueFunc.apply(t); |
| | | } |
| | | |
| | | public static <T, V extends Comparable<? super V>> V getMinValue(List<T> from, Function<T, V> valueFunc) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return null; |
| | | } |
| | | assert from.size() > 0; // 断言,避免告警 |
| | | T t = from.stream().min(Comparator.comparing(valueFunc)).get(); |
| | | return valueFunc.apply(t); |
| | | } |
| | | |
| | | public static <T, V extends Comparable<? super V>> T getMinObject(List<T> from, Function<T, V> valueFunc) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return null; |
| | | } |
| | | assert from.size() > 0; // 断言,避免告警 |
| | | return from.stream().min(Comparator.comparing(valueFunc)).get(); |
| | | } |
| | | |
| | | public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc, |
| | | BinaryOperator<V> accumulator) { |
| | | return getSumValue(from, valueFunc, accumulator, null); |
| | | } |
| | | |
| | | public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc, |
| | | BinaryOperator<V> accumulator, V defaultValue) { |
| | | if (CollUtil.isEmpty(from)) { |
| | | return defaultValue; |
| | | } |
| | | assert !from.isEmpty(); // 断言,避免告警 |
| | | return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue); |
| | | } |
| | | |
| | | public static <T> void addIfNotNull(Collection<T> coll, T item) { |
| | | if (item == null) { |
| | | return; |
| | | } |
| | | coll.add(item); |
| | | } |
| | | |
| | | public static <T> Collection<T> singleton(T obj) { |
| | | return obj == null ? Collections.emptyList() : Collections.singleton(obj); |
| | | } |
| | | |
| | | public static <T> List<T> newArrayList(List<List<T>> list) { |
| | | return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.util.collection; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.collection.CollectionUtil; |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import com.iailab.framework.common.core.KeyValue; |
| | | import com.google.common.collect.Maps; |
| | | import com.google.common.collect.Multimap; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.function.Consumer; |
| | | |
| | | /** |
| | | * Map 工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class MapUtils { |
| | | |
| | | /** |
| | | * 从哈希表表中,获得 keys 对应的所有 value 数组 |
| | | * |
| | | * @param multimap 哈希表 |
| | | * @param keys keys |
| | | * @return value 数组 |
| | | */ |
| | | public static <K, V> List<V> getList(Multimap<K, V> multimap, Collection<K> keys) { |
| | | List<V> result = new ArrayList<>(); |
| | | keys.forEach(k -> { |
| | | Collection<V> values = multimap.get(k); |
| | | if (CollectionUtil.isEmpty(values)) { |
| | | return; |
| | | } |
| | | result.addAll(values); |
| | | }); |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 从哈希表查找到 key 对应的 value,然后进一步处理 |
| | | * key 为 null 时, 不处理 |
| | | * 注意,如果查找到的 value 为 null 时,不进行处理 |
| | | * |
| | | * @param map 哈希表 |
| | | * @param key key |
| | | * @param consumer 进一步处理的逻辑 |
| | | */ |
| | | public static <K, V> void findAndThen(Map<K, V> map, K key, Consumer<V> consumer) { |
| | | if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) { |
| | | return; |
| | | } |
| | | V value = map.get(key); |
| | | if (value == null) { |
| | | return; |
| | | } |
| | | consumer.accept(value); |
| | | } |
| | | |
| | | public static <K, V> Map<K, V> convertMap(List<KeyValue<K, V>> keyValues) { |
| | | Map<K, V> map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); |
| | | keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); |
| | | return map; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.util.collection; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * Set 工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class SetUtils { |
| | | |
| | | @SafeVarargs |
| | | public static <T> Set<T> asSet(T... objs) { |
| | | return CollUtil.newHashSet(objs); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.util.date; |
| | | |
| | | import cn.hutool.core.date.LocalDateTimeUtil; |
| | | import org.joda.time.DateTime; |
| | | |
| | | import java.text.ParseException; |
| | | import java.text.SimpleDateFormat; |
| | | import java.time.*; |
| | | import java.util.ArrayList; |
| | | import java.util.Calendar; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 时间工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DateUtils { |
| | | |
| | | /** |
| | | * 时区 - 默认 |
| | | */ |
| | | public static final String TIME_ZONE_DEFAULT = "GMT+8"; |
| | | |
| | | /** |
| | | * 秒转换成毫秒 |
| | | */ |
| | | public static final long SECOND_MILLIS = 1000; |
| | | |
| | | public static final String FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd"; |
| | | /** 时间格式(yyyy.MM.dd) */ |
| | | public final static String DATE_PATTERN_POINT = "yyyy.MM.dd"; |
| | | |
| | | public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; |
| | | |
| | | public final static String DATE_TIME_PATTERN_STRING = "yyyyMMddHHmmss"; |
| | | |
| | | public static final String FORMAT_SIMPLE_TIME = "HH:mm"; |
| | | |
| | | /** |
| | | * 日期格式化 日期格式为:yyyy-MM-dd |
| | | * @param date 日期 |
| | | * @return 返回yyyy-MM-dd格式日期 |
| | | */ |
| | | public static String format(Date date) { |
| | | return format(date, FORMAT_YEAR_MONTH_DAY); |
| | | } |
| | | |
| | | /** |
| | | * 日期格式化 日期格式为:yyyy-MM-dd |
| | | * @param date 日期 |
| | | * @param pattern 格式,如:DateUtils.DATE_TIME_PATTERN |
| | | * @return 返回yyyy-MM-dd格式日期 |
| | | */ |
| | | public static String format(Date date, String pattern) { |
| | | if(date != null){ |
| | | SimpleDateFormat df = new SimpleDateFormat(pattern); |
| | | return df.format(date); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 日期解析 |
| | | * @param date 日期 |
| | | * @param pattern 格式,如:DateUtils.DATE_TIME_PATTERN |
| | | * @return 返回Date |
| | | */ |
| | | public static Date parse(String date, String pattern) { |
| | | try { |
| | | return new SimpleDateFormat(pattern).parse(date); |
| | | } catch (ParseException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 将 LocalDateTime 转换成 Date |
| | | * |
| | | * @param date LocalDateTime |
| | | * @return LocalDateTime |
| | | */ |
| | | public static Date of(LocalDateTime date) { |
| | | if (date == null) { |
| | | return null; |
| | | } |
| | | // 将此日期时间与时区相结合以创建 ZonedDateTime |
| | | ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault()); |
| | | // 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳 |
| | | Instant instant = zonedDateTime.toInstant(); |
| | | // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 |
| | | return Date.from(instant); |
| | | } |
| | | |
| | | /** |
| | | * 将 Date 转换成 LocalDateTime |
| | | * |
| | | * @param date Date |
| | | * @return LocalDateTime |
| | | */ |
| | | public static LocalDateTime of(Date date) { |
| | | if (date == null) { |
| | | return null; |
| | | } |
| | | // 转为时间戳 |
| | | Instant instant = date.toInstant(); |
| | | // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 |
| | | return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); |
| | | } |
| | | |
| | | public static Date addTime(Duration duration) { |
| | | return new Date(System.currentTimeMillis() + duration.toMillis()); |
| | | } |
| | | |
| | | public static boolean isExpired(LocalDateTime time) { |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | return now.isAfter(time); |
| | | } |
| | | |
| | | /** |
| | | * 创建指定时间 |
| | | * |
| | | * @param year 年 |
| | | * @param mouth 月 |
| | | * @param day 日 |
| | | * @return 指定时间 |
| | | */ |
| | | public static Date buildTime(int year, int mouth, int day) { |
| | | return buildTime(year, mouth, day, 0, 0, 0); |
| | | } |
| | | |
| | | /** |
| | | * 创建指定时间 |
| | | * |
| | | * @param year 年 |
| | | * @param mouth 月 |
| | | * @param day 日 |
| | | * @param hour 小时 |
| | | * @param minute 分钟 |
| | | * @param second 秒 |
| | | * @return 指定时间 |
| | | */ |
| | | public static Date buildTime(int year, int mouth, int day, |
| | | int hour, int minute, int second) { |
| | | Calendar calendar = Calendar.getInstance(); |
| | | calendar.set(Calendar.YEAR, year); |
| | | calendar.set(Calendar.MONTH, mouth - 1); |
| | | calendar.set(Calendar.DAY_OF_MONTH, day); |
| | | calendar.set(Calendar.HOUR_OF_DAY, hour); |
| | | calendar.set(Calendar.MINUTE, minute); |
| | | calendar.set(Calendar.SECOND, second); |
| | | calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒 |
| | | return calendar.getTime(); |
| | | } |
| | | |
| | | public static Date max(Date a, Date b) { |
| | | if (a == null) { |
| | | return b; |
| | | } |
| | | if (b == null) { |
| | | return a; |
| | | } |
| | | return a.compareTo(b) > 0 ? a : b; |
| | | } |
| | | |
| | | public static LocalDateTime max(LocalDateTime a, LocalDateTime b) { |
| | | if (a == null) { |
| | | return b; |
| | | } |
| | | if (b == null) { |
| | | return a; |
| | | } |
| | | return a.isAfter(b) ? a : b; |
| | | } |
| | | |
| | | /** |
| | | * 是否今天 |
| | | * |
| | | * @param date 日期 |
| | | * @return 是否 |
| | | */ |
| | | public static boolean isToday(LocalDateTime date) { |
| | | return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now()); |
| | | } |
| | | |
| | | /** |
| | | * 是否昨天 |
| | | * |
| | | * @param date 日期 |
| | | * @return 是否 |
| | | */ |
| | | public static boolean isYesterday(LocalDateTime date) { |
| | | return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now().minusDays(1)); |
| | | } |
| | | |
| | | public static List<String> getTimeScale(Date startDate, Date endDate, int seconds) { |
| | | List<String> days = new ArrayList<String>(); |
| | | Calendar calendar = Calendar.getInstance(); |
| | | calendar.setTime(startDate); |
| | | while (calendar.getTime().compareTo(endDate) <= 0) { |
| | | days.add(DateUtils.format(calendar.getTime(), FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)); |
| | | calendar.add(Calendar.SECOND, seconds); |
| | | } |
| | | return days; |
| | | } |
| | | |
| | | public static List<String> getTimeScale(Date startDate, Date endDate, int seconds, String timeFormat) { |
| | | List<String> days = new ArrayList<String>(); |
| | | Calendar calendar = Calendar.getInstance(); |
| | | calendar.setTime(startDate); |
| | | while (calendar.getTime().compareTo(endDate) <= 0) { |
| | | days.add(DateUtils.format(calendar.getTime(), timeFormat)); |
| | | calendar.add(Calendar.SECOND, seconds); |
| | | } |
| | | return days; |
| | | } |
| | | |
| | | /** |
| | | * 对日期的【秒】进行加/减 |
| | | * |
| | | * @param date 日期 |
| | | * @param seconds 秒数,负数为减 |
| | | * @return 加/减几秒后的日期 |
| | | */ |
| | | public static Date addDateSeconds(Date date, int seconds) { |
| | | DateTime dateTime = new DateTime(date); |
| | | return dateTime.plusSeconds(seconds).toDate(); |
| | | } |
| | | |
| | | /** |
| | | * 对日期的【分钟】进行加/减 |
| | | * |
| | | * @param date 日期 |
| | | * @param minutes 分钟数,负数为减 |
| | | * @return 加/减几分钟后的日期 |
| | | */ |
| | | public static Date addDateMinutes(Date date, int minutes) { |
| | | DateTime dateTime = new DateTime(date); |
| | | return dateTime.plusMinutes(minutes).toDate(); |
| | | } |
| | | |
| | | /** |
| | | * 对日期的【小时】进行加/减 |
| | | * |
| | | * @param date 日期 |
| | | * @param hours 小时数,负数为减 |
| | | * @return 加/减几小时后的日期 |
| | | */ |
| | | public static Date addDateHours(Date date, int hours) { |
| | | DateTime dateTime = new DateTime(date); |
| | | return dateTime.plusHours(hours).toDate(); |
| | | } |
| | | |
| | | /** |
| | | * 对日期的【天】进行加/减 |
| | | * |
| | | * @param date 日期 |
| | | * @param days 天数,负数为减 |
| | | * @return 加/减几天后的日期 |
| | | */ |
| | | public static Date addDateDays(Date date, int days) { |
| | | DateTime dateTime = new DateTime(date); |
| | | return dateTime.plusDays(days).toDate(); |
| | | } |
| | | |
| | | /** |
| | | * 对日期的【周】进行加/减 |
| | | * |
| | | * @param date 日期 |
| | | * @param weeks 周数,负数为减 |
| | | * @return 加/减几周后的日期 |
| | | */ |
| | | public static Date addDateWeeks(Date date, int weeks) { |
| | | DateTime dateTime = new DateTime(date); |
| | | return dateTime.plusWeeks(weeks).toDate(); |
| | | } |
| | | |
| | | /** |
| | | * 对日期的【月】进行加/减 |
| | | * |
| | | * @param date 日期 |
| | | * @param months 月数,负数为减 |
| | | * @return 加/减几月后的日期 |
| | | */ |
| | | public static Date addDateMonths(Date date, int months) { |
| | | DateTime dateTime = new DateTime(date); |
| | | return dateTime.plusMonths(months).toDate(); |
| | | } |
| | | |
| | | /** |
| | | * 对日期的【年】进行加/减 |
| | | * |
| | | * @param date 日期 |
| | | * @param years 年数,负数为减 |
| | | * @return 加/减几年后的日期 |
| | | */ |
| | | public static Date addDateYears(Date date, int years) { |
| | | DateTime dateTime = new DateTime(date); |
| | | return dateTime.plusYears(years).toDate(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.util.date; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.date.DatePattern; |
| | | import cn.hutool.core.date.LocalDateTimeUtil; |
| | | import cn.hutool.core.lang.Assert; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.enums.DateIntervalEnum; |
| | | |
| | | import java.time.*; |
| | | import java.time.format.DateTimeParseException; |
| | | import java.time.temporal.ChronoUnit; |
| | | import java.time.temporal.TemporalAdjusters; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 时间工具类,用于 {@link java.time.LocalDateTime} |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class LocalDateTimeUtils { |
| | | |
| | | /** |
| | | * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值 |
| | | */ |
| | | public static LocalDateTime EMPTY = buildTime(1970, 1, 1); |
| | | |
| | | /** |
| | | * 解析时间 |
| | | * |
| | | * 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功 |
| | | * |
| | | * @param time 时间 |
| | | * @return 时间字符串 |
| | | */ |
| | | public static LocalDateTime parse(String time) { |
| | | try { |
| | | return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN); |
| | | } catch (DateTimeParseException e) { |
| | | return LocalDateTimeUtil.parse(time); |
| | | } |
| | | } |
| | | |
| | | public static LocalDateTime addTime(Duration duration) { |
| | | return LocalDateTime.now().plus(duration); |
| | | } |
| | | |
| | | public static LocalDateTime minusTime(Duration duration) { |
| | | return LocalDateTime.now().minus(duration); |
| | | } |
| | | |
| | | public static boolean beforeNow(LocalDateTime date) { |
| | | return date.isBefore(LocalDateTime.now()); |
| | | } |
| | | |
| | | public static boolean afterNow(LocalDateTime date) { |
| | | return date.isAfter(LocalDateTime.now()); |
| | | } |
| | | |
| | | /** |
| | | * 创建指定时间 |
| | | * |
| | | * @param year 年 |
| | | * @param mouth 月 |
| | | * @param day 日 |
| | | * @return 指定时间 |
| | | */ |
| | | public static LocalDateTime buildTime(int year, int mouth, int day) { |
| | | return LocalDateTime.of(year, mouth, day, 0, 0, 0); |
| | | } |
| | | |
| | | public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1, |
| | | int year2, int mouth2, int day2) { |
| | | return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)}; |
| | | } |
| | | |
| | | /** |
| | | * 判指定断时间,是否在该时间范围内 |
| | | * |
| | | * @param startTime 开始时间 |
| | | * @param endTime 结束时间 |
| | | * @param time 指定时间 |
| | | * @return 是否 |
| | | */ |
| | | public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) { |
| | | if (startTime == null || endTime == null || time == null) { |
| | | return false; |
| | | } |
| | | return LocalDateTimeUtil.isIn(parse(time), startTime, endTime); |
| | | } |
| | | |
| | | /** |
| | | * 判断当前时间是否在该时间范围内 |
| | | * |
| | | * @param startTime 开始时间 |
| | | * @param endTime 结束时间 |
| | | * @return 是否 |
| | | */ |
| | | public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) { |
| | | if (startTime == null || endTime == null) { |
| | | return false; |
| | | } |
| | | return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime); |
| | | } |
| | | |
| | | /** |
| | | * 判断当前时间是否在该时间范围内 |
| | | * |
| | | * @param startTime 开始时间 |
| | | * @param endTime 结束时间 |
| | | * @return 是否 |
| | | */ |
| | | public static boolean isBetween(String startTime, String endTime) { |
| | | if (startTime == null || endTime == null) { |
| | | return false; |
| | | } |
| | | LocalDate nowDate = LocalDate.now(); |
| | | return LocalDateTimeUtil.isIn(LocalDateTime.now(), |
| | | LocalDateTime.of(nowDate, LocalTime.parse(startTime)), |
| | | LocalDateTime.of(nowDate, LocalTime.parse(endTime))); |
| | | } |
| | | |
| | | /** |
| | | * 判断时间段是否重叠 |
| | | * |
| | | * @param startTime1 开始 time1 |
| | | * @param endTime1 结束 time1 |
| | | * @param startTime2 开始 time2 |
| | | * @param endTime2 结束 time2 |
| | | * @return 重叠:true 不重叠:false |
| | | */ |
| | | public static boolean isOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) { |
| | | LocalDate nowDate = LocalDate.now(); |
| | | return LocalDateTimeUtil.isOverlap(LocalDateTime.of(nowDate, startTime1), LocalDateTime.of(nowDate, endTime1), |
| | | LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2)); |
| | | } |
| | | |
| | | /** |
| | | * 获取指定日期所在的月份的开始时间 |
| | | * 例如:2023-09-30 00:00:00,000 |
| | | * |
| | | * @param date 日期 |
| | | * @return 月份的开始时间 |
| | | */ |
| | | public static LocalDateTime beginOfMonth(LocalDateTime date) { |
| | | return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN); |
| | | } |
| | | |
| | | /** |
| | | * 获取指定日期所在的月份的最后时间 |
| | | * 例如:2023-09-30 23:59:59,999 |
| | | * |
| | | * @param date 日期 |
| | | * @return 月份的结束时间 |
| | | */ |
| | | public static LocalDateTime endOfMonth(LocalDateTime date) { |
| | | return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX); |
| | | } |
| | | |
| | | /** |
| | | * 获得指定日期所在季度 |
| | | * |
| | | * @param date 日期 |
| | | * @return 所在季度 |
| | | */ |
| | | public static int getQuarterOfYear(LocalDateTime date) { |
| | | return (date.getMonthValue() - 1) / 3 + 1; |
| | | } |
| | | |
| | | /** |
| | | * 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负 |
| | | * |
| | | * @param dateTime 日期 |
| | | * @return 相差天数 |
| | | */ |
| | | public static Long between(LocalDateTime dateTime) { |
| | | return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS); |
| | | } |
| | | |
| | | /** |
| | | * 获取今天的开始时间 |
| | | * |
| | | * @return 今天 |
| | | */ |
| | | public static LocalDateTime getToday() { |
| | | return LocalDateTimeUtil.beginOfDay(LocalDateTime.now()); |
| | | } |
| | | |
| | | /** |
| | | * 获取昨天的开始时间 |
| | | * |
| | | * @return 昨天 |
| | | */ |
| | | public static LocalDateTime getYesterday() { |
| | | return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1)); |
| | | } |
| | | |
| | | /** |
| | | * 获取本月的开始时间 |
| | | * |
| | | * @return 本月 |
| | | */ |
| | | public static LocalDateTime getMonth() { |
| | | return beginOfMonth(LocalDateTime.now()); |
| | | } |
| | | |
| | | /** |
| | | * 获取本年的开始时间 |
| | | * |
| | | * @return 本年 |
| | | */ |
| | | public static LocalDateTime getYear() { |
| | | return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN); |
| | | } |
| | | |
| | | public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime, |
| | | LocalDateTime endTime, |
| | | Integer interval) { |
| | | // 1.1 找到枚举 |
| | | DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); |
| | | Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); |
| | | // 1.2 将时间对齐 |
| | | startTime = LocalDateTimeUtil.beginOfDay(startTime); |
| | | endTime = LocalDateTimeUtil.endOfDay(endTime); |
| | | |
| | | // 2. 循环,生成时间范围 |
| | | List<LocalDateTime[]> timeRanges = new ArrayList<>(); |
| | | switch (intervalEnum) { |
| | | case DAY: |
| | | while (startTime.isBefore(endTime)) { |
| | | timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)}); |
| | | startTime = startTime.plusDays(1); |
| | | } |
| | | break; |
| | | case WEEK: |
| | | while (startTime.isBefore(endTime)) { |
| | | LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1); |
| | | timeRanges.add(new LocalDateTime[]{startTime, endOfWeek}); |
| | | startTime = endOfWeek.plusNanos(1); |
| | | } |
| | | break; |
| | | case MONTH: |
| | | while (startTime.isBefore(endTime)) { |
| | | LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1); |
| | | timeRanges.add(new LocalDateTime[]{startTime, endOfMonth}); |
| | | startTime = endOfMonth.plusNanos(1); |
| | | } |
| | | break; |
| | | case QUARTER: |
| | | while (startTime.isBefore(endTime)) { |
| | | int quarterOfYear = getQuarterOfYear(startTime); |
| | | LocalDateTime quarterEnd = quarterOfYear == 4 |
| | | ? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1) |
| | | : startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1); |
| | | timeRanges.add(new LocalDateTime[]{startTime, quarterEnd}); |
| | | startTime = quarterEnd.plusNanos(1); |
| | | } |
| | | break; |
| | | case YEAR: |
| | | while (startTime.isBefore(endTime)) { |
| | | LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1); |
| | | timeRanges.add(new LocalDateTime[]{startTime, endOfYear}); |
| | | startTime = endOfYear.plusNanos(1); |
| | | } |
| | | break; |
| | | default: |
| | | throw new IllegalArgumentException("Invalid interval: " + interval); |
| | | } |
| | | // 3. 兜底,最后一个时间,需要保持在 endTime 之前 |
| | | LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges); |
| | | if (lastTimeRange != null) { |
| | | lastTimeRange[1] = endTime; |
| | | } |
| | | return timeRanges; |
| | | } |
| | | |
| | | /** |
| | | * 格式化时间范围 |
| | | * |
| | | * @param startTime 开始时间 |
| | | * @param endTime 结束时间 |
| | | * @param interval 时间间隔 |
| | | * @return 时间范围 |
| | | */ |
| | | public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) { |
| | | // 1. 找到枚举 |
| | | DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); |
| | | Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); |
| | | |
| | | // 2. 循环,生成时间范围 |
| | | switch (intervalEnum) { |
| | | case DAY: |
| | | return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN); |
| | | case WEEK: |
| | | return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN) |
| | | + StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime)); |
| | | case MONTH: |
| | | return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN); |
| | | case QUARTER: |
| | | return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime)); |
| | | case YEAR: |
| | | return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN); |
| | | default: |
| | | throw new IllegalArgumentException("Invalid interval: " + interval); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.common.util.http; |
| | | |
| | | import cn.hutool.core.codec.Base64; |
| | | import cn.hutool.core.map.TableMap; |
| | | import cn.hutool.core.net.url.UrlBuilder; |
| | | import cn.hutool.core.util.ReflectUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import org.springframework.util.CollectionUtils; |
| | | import org.springframework.util.StringUtils; |
| | | import org.springframework.web.util.UriComponents; |
| | | import org.springframework.web.util.UriComponentsBuilder; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.io.*; |
| | | import java.net.HttpURLConnection; |
| | | import java.net.URI; |
| | | import java.net.URL; |
| | | import java.net.URLConnection; |
| | | import java.nio.charset.Charset; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * HTTP 工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class HttpUtils { |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | public static String replaceUrlQuery(String url, String key, String value) { |
| | | UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); |
| | | // 先移除 |
| | | TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>) |
| | | ReflectUtil.getFieldValue(builder.getQuery(), "query"); |
| | | query.remove(key); |
| | | // 后添加 |
| | | builder.addQuery(key, value); |
| | | return builder.build(); |
| | | } |
| | | |
| | | private String append(String base, Map<String, ?> query, boolean fragment) { |
| | | return append(base, query, null, fragment); |
| | | } |
| | | |
| | | /** |
| | | * 拼接 URL |
| | | * |
| | | * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法 |
| | | * |
| | | * @param base 基础 URL |
| | | * @param query 查询参数 |
| | | * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射 |
| | | * @param fragment URL 的 fragment,即拼接到 # 中 |
| | | * @return 拼接后的 URL |
| | | */ |
| | | public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) { |
| | | UriComponentsBuilder template = UriComponentsBuilder.newInstance(); |
| | | UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base); |
| | | URI redirectUri; |
| | | try { |
| | | // assume it's encoded to start with (if it came in over the wire) |
| | | redirectUri = builder.build(true).toUri(); |
| | | } catch (Exception e) { |
| | | // ... but allow client registrations to contain hard-coded non-encoded values |
| | | redirectUri = builder.build().toUri(); |
| | | builder = UriComponentsBuilder.fromUri(redirectUri); |
| | | } |
| | | template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost()) |
| | | .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath()); |
| | | |
| | | if (fragment) { |
| | | StringBuilder values = new StringBuilder(); |
| | | if (redirectUri.getFragment() != null) { |
| | | String append = redirectUri.getFragment(); |
| | | values.append(append); |
| | | } |
| | | for (String key : query.keySet()) { |
| | | if (values.length() > 0) { |
| | | values.append("&"); |
| | | } |
| | | String name = key; |
| | | if (keys != null && keys.containsKey(key)) { |
| | | name = keys.get(key); |
| | | } |
| | | values.append(name).append("={").append(key).append("}"); |
| | | } |
| | | if (values.length() > 0) { |
| | | template.fragment(values.toString()); |
| | | } |
| | | UriComponents encoded = template.build().expand(query).encode(); |
| | | builder.fragment(encoded.getFragment()); |
| | | } else { |
| | | for (String key : query.keySet()) { |
| | | String name = key; |
| | | if (keys != null && keys.containsKey(key)) { |
| | | name = keys.get(key); |
| | | } |
| | | template.queryParam(name, "{" + key + "}"); |
| | | } |
| | | template.fragment(redirectUri.getFragment()); |
| | | UriComponents encoded = template.build().expand(query).encode(); |
| | | builder.query(encoded.getQuery()); |
| | | } |
| | | return builder.build().toUriString(); |
| | | } |
| | | |
| | | public static String[] obtainBasicAuthorization(HttpServletRequest request) { |
| | | String clientId; |
| | | String clientSecret; |
| | | // 先从 Header 中获取 |
| | | String authorization = request.getHeader("Authorization"); |
| | | authorization = StrUtil.subAfter(authorization, "Basic ", true); |
| | | if (StringUtils.hasText(authorization)) { |
| | | authorization = Base64.decodeStr(authorization); |
| | | clientId = StrUtil.subBefore(authorization, ":", false); |
| | | clientSecret = StrUtil.subAfter(authorization, ":", false); |
| | | // 再从 Param 中获取 |
| | | } else { |
| | | clientId = request.getParameter("client_id"); |
| | | clientSecret = request.getParameter("client_secret"); |
| | | } |
| | | |
| | | // 如果两者非空,则返回 |
| | | if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) { |
| | | return new String[]{clientId, clientSecret}; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 向指定URL发送GET方法的请求 |
| | | * |
| | | * @param url 发送请求的URL |
| | | * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 |
| | | * @return URL 所代表远程资源的响应结果 |
| | | */ |
| | | public static String sendGet(String url, String param) { |
| | | String result = ""; |
| | | BufferedReader in = null; |
| | | try { |
| | | String urlNameString = url + "?" + param; |
| | | URL realUrl = new URL(urlNameString); |
| | | // 打开和URL之间的连接 |
| | | URLConnection connection = realUrl.openConnection(); |
| | | // 设置通用的请求属性 |
| | | connection.setRequestProperty("accept", "*/*"); |
| | | connection.setRequestProperty("connection", "Keep-Alive"); |
| | | connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); |
| | | |
| | | // 建立实际的连接 |
| | | connection.connect(); |
| | | // 获取所有响应头字段 |
| | | Map<String, List<String>> map = connection.getHeaderFields(); |
| | | // 遍历所有的响应头字段 |
| | | for (String key : map.keySet()) { |
| | | System.out.println(key + "--->" + map.get(key)); |
| | | } |
| | | // 定义 BufferedReader输入流来读取URL的响应 |
| | | in = new BufferedReader(new InputStreamReader(connection.getInputStream())); |
| | | String line; |
| | | while ((line = in.readLine()) != null) { |
| | | result += line; |
| | | } |
| | | } catch (Exception e) { |
| | | System.out.println("发送GET请求出现异常!" + e); |
| | | e.printStackTrace(); |
| | | } |
| | | // 使用finally块来关闭输入流 |
| | | finally { |
| | | try { |
| | | if (in != null) { |
| | | in.close(); |
| | | } |
| | | } catch (Exception e2) { |
| | | e2.printStackTrace(); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | public static String sendGet(String url, Map<String, String> params, String authorization) { |
| | | String result = ""; |
| | | BufferedReader in = null; |
| | | try { |
| | | StringBuilder sb = new StringBuilder(); |
| | | sb.append(url); |
| | | if (!CollectionUtils.isEmpty(params)) { |
| | | sb.append("?"); |
| | | params.forEach((k, v) -> { |
| | | sb.append(k + "=" + v + "&"); |
| | | }); |
| | | sb.append("t=" + System.currentTimeMillis()); |
| | | } |
| | | String urlNameString = sb.toString(); |
| | | URL realUrl = new URL(urlNameString); |
| | | // 打开和URL之间的连接 |
| | | URLConnection connection = realUrl.openConnection(); |
| | | // 设置通用的请求属性 |
| | | connection.setRequestProperty("Authorization", authorization); |
| | | connection.setRequestProperty("accept", "*/*"); |
| | | connection.setRequestProperty("connection", "Keep-Alive"); |
| | | connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); |
| | | |
| | | // 建立实际的连接 |
| | | connection.connect(); |
| | | // 获取所有响应头字段 |
| | | Map<String, List<String>> map = connection.getHeaderFields(); |
| | | // 遍历所有的响应头字段 |
| | | for (String key : map.keySet()) { |
| | | System.out.println(key + "--->" + map.get(key)); |
| | | } |
| | | // 定义 BufferedReader输入流来读取URL的响应 |
| | | in = new BufferedReader(new InputStreamReader(connection.getInputStream())); |
| | | String line; |
| | | while ((line = in.readLine()) != null) { |
| | | result += line; |
| | | } |
| | | } catch (Exception e) { |
| | | System.out.println("发送GET请求出现异常!" + e); |
| | | e.printStackTrace(); |
| | | } |
| | | // 使用finally块来关闭输入流 |
| | | finally { |
| | | try { |
| | | if (in != null) { |
| | | in.close(); |
| | | } |
| | | } catch (Exception e2) { |
| | | e2.printStackTrace(); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 向指定 URL 发送POST方法的请求 |
| | | * |
| | | * @param url |
| | | * @param json |
| | | * @return |
| | | */ |
| | | public static String sendPost(String url, String json) { |
| | | PrintWriter out = null; |
| | | BufferedReader in = null; |
| | | String result = ""; |
| | | try { |
| | | URL realUrl = new URL(url); |
| | | // 打开和URL之间的连接 |
| | | URLConnection conn = realUrl.openConnection(); |
| | | // 设置通用的请求属性 |
| | | conn.setRequestProperty("content-type", "application/json"); |
| | | conn.setRequestProperty("accept", "*/*"); |
| | | conn.setRequestProperty("connection", "Keep-Alive"); |
| | | conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); |
| | | |
| | | // 发送POST请求必须设置如下两行 |
| | | conn.setDoOutput(true); |
| | | conn.setDoInput(true); |
| | | // 获取URLConnection对象对应的输出流 |
| | | out = new PrintWriter(conn.getOutputStream()); |
| | | // 发送请求参数 |
| | | out.print(json); |
| | | // flush输出流的缓冲 |
| | | out.flush(); |
| | | // 定义BufferedReader输入流来读取URL的响应 |
| | | in = new BufferedReader(new InputStreamReader(conn.getInputStream())); |
| | | String line; |
| | | while ((line = in.readLine()) != null) { |
| | | result += line; |
| | | } |
| | | } catch (Exception e) { |
| | | System.out.println("发送 POST 请求出现异常!" + e); |
| | | e.printStackTrace(); |
| | | } |
| | | //使用finally块来关闭输出流、输入流 |
| | | finally { |
| | | try { |
| | | if (out != null) { |
| | | out.close(); |
| | | } |
| | | if (in != null) { |
| | | in.close(); |
| | | } |
| | | } catch (IOException ex) { |
| | | ex.printStackTrace(); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 向指定 URL 发送POST方法的请求 |
| | | * |
| | | * @param url |
| | | * @param json |
| | | * @param authorization |
| | | * @return |
| | | */ |
| | | public static String sendPost(String url, String json, String authorization) { |
| | | PrintWriter out = null; |
| | | BufferedReader in = null; |
| | | String result = ""; |
| | | try { |
| | | URL realUrl = new URL(url); |
| | | // 打开和URL之间的连接 |
| | | URLConnection conn = realUrl.openConnection(); |
| | | // 设置通用的请求属性 |
| | | conn.setRequestProperty("Authorization", authorization); |
| | | conn.setRequestProperty("accept", "*/*"); |
| | | conn.setRequestProperty("connection", "Keep-Alive"); |
| | | conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); |
| | | |
| | | // 发送POST请求必须设置如下两行 |
| | | conn.setDoOutput(true); |
| | | conn.setDoInput(true); |
| | | // 获取URLConnection对象对应的输出流 |
| | | out = new PrintWriter(conn.getOutputStream()); |
| | | // 发送请求参数 |
| | | out.print(json); |
| | | // flush输出流的缓冲 |
| | | out.flush(); |
| | | // 定义BufferedReader输入流来读取URL的响应 |
| | | in = new BufferedReader(new InputStreamReader(conn.getInputStream())); |
| | | String line; |
| | | while ((line = in.readLine()) != null) { |
| | | result += line; |
| | | } |
| | | } catch (Exception e) { |
| | | System.out.println("发送 POST 请求出现异常!" + e); |
| | | e.printStackTrace(); |
| | | } |
| | | //使用finally块来关闭输出流、输入流 |
| | | finally { |
| | | try { |
| | | if (out != null) { |
| | | out.close(); |
| | | } |
| | | if (in != null) { |
| | | in.close(); |
| | | } |
| | | } catch (IOException ex) { |
| | | ex.printStackTrace(); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | public static String sendPostToken(String url, String json, String authorization) { |
| | | String result = ""; |
| | | try { |
| | | URL realUrl = new URL(url); |
| | | HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection(); |
| | | connection.setRequestMethod("POST"); |
| | | connection.setRequestProperty("Content-Type", "application/json"); |
| | | connection.setRequestProperty("Authorization", authorization); |
| | | connection.setDoOutput(true); |
| | | |
| | | // 发送POST请求的数据 |
| | | try (OutputStream os = connection.getOutputStream()) { |
| | | os.write(json.getBytes()); |
| | | os.flush(); |
| | | } |
| | | // 获取响应码和响应体 |
| | | int responseCode = connection.getResponseCode(); |
| | | if (responseCode == HttpURLConnection.HTTP_OK) { // 200 OK |
| | | try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { |
| | | StringBuilder response = new StringBuilder(); |
| | | String responseLine; |
| | | while ((responseLine = br.readLine()) != null) { |
| | | response.append(responseLine.trim()); |
| | | } |
| | | result = response.toString(); |
| | | } |
| | | } else { |
| | | System.out.println("POST request not worked"); |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | |
| | | } |
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/FileUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/io/IoUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/JsonUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/NumberSerializer.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/monitor/TracerUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/MoneyUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/number/NumberUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/BeanUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ConvertUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/ObjectUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/object/PageUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/package-info.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/servlet/ServletUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringContextUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringExpressionUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/spring/SpringUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/string/StrUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/validation/ValidationUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnum.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumCollectionValidator.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/InEnumValidator.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Mobile.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/MobileValidator.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/Telephone.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/TelephoneValidator.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/AddGroup.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/DefaultGroup.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/Group.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/group/UpdateGroup.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/validation/package-info.java
iailab-framework/iailab-common/src/test/java/com/iailab/framework/common/util/collection/CollectionUtilsTest.java
iailab-framework/pom.xml |