对比新文件 |
| | |
| | | <?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-data-permission</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> |
| | | <optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 --> |
| | | </dependency> |
| | | |
| | | <!-- DB 相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-mybatis</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> |
| | | |
| | | <!-- 业务组件 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-module-system-api</artifactId> <!-- 需要使用它,进行数据权限的获取 --> |
| | | <version>${revision}</version> |
| | | </dependency> |
| | | |
| | | <!-- Test 测试相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-test</artifactId> |
| | | <scope>test</scope> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.config; |
| | | |
| | | import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; |
| | | import com.iailab.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; |
| | | import com.iailab.framework.datapermission.core.db.DataPermissionRuleHandler; |
| | | import com.iailab.framework.datapermission.core.rule.DataPermissionRule; |
| | | import com.iailab.framework.datapermission.core.rule.DataPermissionRuleFactory; |
| | | import com.iailab.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; |
| | | import com.iailab.framework.mybatis.core.util.MyBatisUtils; |
| | | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 数据权限的自动配置类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | public class IailabDataPermissionAutoConfiguration { |
| | | |
| | | @Bean |
| | | public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) { |
| | | return new DataPermissionRuleFactoryImpl(rules); |
| | | } |
| | | |
| | | @Bean |
| | | public DataPermissionRuleHandler dataPermissionRuleHandler(MybatisPlusInterceptor interceptor, |
| | | DataPermissionRuleFactory ruleFactory) { |
| | | // 创建 DataPermissionInterceptor 拦截器 |
| | | DataPermissionRuleHandler handler = new DataPermissionRuleHandler(ruleFactory); |
| | | DataPermissionInterceptor inner = new DataPermissionInterceptor(handler); |
| | | // 添加到 interceptor 中 |
| | | // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 |
| | | MyBatisUtils.addInterceptor(interceptor, inner, 0); |
| | | return handler; |
| | | } |
| | | |
| | | |
| | | @Bean |
| | | public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { |
| | | return new DataPermissionAnnotationAdvisor(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.config; |
| | | |
| | | import com.iailab.framework.datapermission.core.rpc.DataPermissionRequestInterceptor; |
| | | import com.iailab.framework.datapermission.core.rpc.DataPermissionRpcWebFilter; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
| | | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | import static com.iailab.framework.common.enums.WebFilterOrderEnum.TENANT_CONTEXT_FILTER; |
| | | |
| | | |
| | | /** |
| | | * 数据权限针对 RPC 的自动配置类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @ConditionalOnClass(name = "feign.RequestInterceptor") |
| | | public class IailabDataPermissionRpcAutoConfiguration { |
| | | |
| | | @Bean |
| | | public DataPermissionRequestInterceptor dataPermissionRequestInterceptor() { |
| | | return new DataPermissionRequestInterceptor(); |
| | | } |
| | | |
| | | @Bean |
| | | public FilterRegistrationBean<DataPermissionRpcWebFilter> dataPermissionRpcFilter() { |
| | | FilterRegistrationBean<DataPermissionRpcWebFilter> registrationBean = new FilterRegistrationBean<>(); |
| | | registrationBean.setFilter(new DataPermissionRpcWebFilter()); |
| | | registrationBean.setOrder(TENANT_CONTEXT_FILTER - 1); // 顺序没有绝对的要求,在租户 Filter 前面稳妥点 |
| | | return registrationBean; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.config; |
| | | |
| | | import cn.hutool.extra.spring.SpringUtil; |
| | | import com.iailab.framework.datapermission.core.rule.dept.DeptDataPermissionRule; |
| | | import com.iailab.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; |
| | | import com.iailab.framework.security.core.LoginUser; |
| | | import com.iailab.module.system.api.permission.PermissionApi; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 基于部门的数据权限 AutoConfiguration |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AutoConfiguration |
| | | @ConditionalOnClass(LoginUser.class) |
| | | @ConditionalOnBean(value = DeptDataPermissionRuleCustomizer.class) |
| | | public class IailabDeptDataPermissionAutoConfiguration { |
| | | |
| | | @Bean |
| | | public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi, |
| | | List<DeptDataPermissionRuleCustomizer> customizers) { |
| | | // Cloud 专属逻辑:优先使用本地的 PermissionApi 实现类,而不是 Feign 调用 |
| | | // 原因:在创建租户时,租户还没创建好,导致 Feign 调用获取数据权限时,报“租户不存在”的错误 |
| | | try { |
| | | PermissionApi permissionApiImpl = SpringUtil.getBean("permissionApiImpl", PermissionApi.class); |
| | | if (permissionApiImpl != null) { |
| | | permissionApi = permissionApiImpl; |
| | | } |
| | | } catch (Exception ignored) {} |
| | | |
| | | // 创建 DeptDataPermissionRule 对象 |
| | | DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi); |
| | | // 补全表配置 |
| | | customizers.forEach(customizer -> customizer.customize(rule)); |
| | | return rule; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.annotation; |
| | | |
| | | import com.iailab.framework.datapermission.core.rule.DataPermissionRule; |
| | | |
| | | import java.lang.annotation.*; |
| | | |
| | | /** |
| | | * 数据权限注解 |
| | | * 可声明在类或者方法上,标识使用的数据权限规则 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Target({ElementType.TYPE, ElementType.METHOD}) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Documented |
| | | public @interface DataPermission { |
| | | |
| | | /** |
| | | * 当前类或方法是否开启数据权限 |
| | | * 即使不添加 @DataPermission 注解,默认是开启状态 |
| | | * 可通过设置 enable 为 false 禁用 |
| | | */ |
| | | boolean enable() default true; |
| | | |
| | | /** |
| | | * 生效的数据权限规则数组,优先级高于 {@link #excludeRules()} |
| | | */ |
| | | Class<? extends DataPermissionRule>[] includeRules() default {}; |
| | | |
| | | /** |
| | | * 排除的数据权限规则数组,优先级最低 |
| | | */ |
| | | Class<? extends DataPermissionRule>[] excludeRules() default {}; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.aop; |
| | | |
| | | import com.iailab.framework.datapermission.core.annotation.DataPermission; |
| | | import lombok.EqualsAndHashCode; |
| | | import lombok.Getter; |
| | | import org.aopalliance.aop.Advice; |
| | | import org.springframework.aop.Pointcut; |
| | | import org.springframework.aop.support.AbstractPointcutAdvisor; |
| | | import org.springframework.aop.support.ComposablePointcut; |
| | | import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; |
| | | |
| | | /** |
| | | * {@link com.iailab.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Getter |
| | | @EqualsAndHashCode(callSuper = true) |
| | | public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor { |
| | | |
| | | private final Advice advice; |
| | | |
| | | private final Pointcut pointcut; |
| | | |
| | | public DataPermissionAnnotationAdvisor() { |
| | | this.advice = new DataPermissionAnnotationInterceptor(); |
| | | this.pointcut = this.buildPointcut(); |
| | | } |
| | | |
| | | protected Pointcut buildPointcut() { |
| | | Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true); |
| | | Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true); |
| | | return new ComposablePointcut(classPointcut).union(methodPointcut); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.aop; |
| | | |
| | | import com.iailab.framework.datapermission.core.annotation.DataPermission; |
| | | import lombok.Getter; |
| | | import org.aopalliance.intercept.MethodInterceptor; |
| | | import org.aopalliance.intercept.MethodInvocation; |
| | | import org.springframework.core.MethodClassKey; |
| | | import org.springframework.core.annotation.AnnotationUtils; |
| | | |
| | | import java.lang.reflect.Method; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | /** |
| | | * {@link DataPermission} 注解的拦截器 |
| | | * 1. 在执行方法前,将 @DataPermission 注解入栈 |
| | | * 2. 在执行方法后,将 @DataPermission 注解出栈 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象 |
| | | public class DataPermissionAnnotationInterceptor implements MethodInterceptor { |
| | | |
| | | /** |
| | | * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位 |
| | | */ |
| | | static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class); |
| | | |
| | | @Getter |
| | | private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>(); |
| | | |
| | | @Override |
| | | public Object invoke(MethodInvocation methodInvocation) throws Throwable { |
| | | // 入栈 |
| | | DataPermission dataPermission = this.findAnnotation(methodInvocation); |
| | | if (dataPermission != null) { |
| | | DataPermissionContextHolder.add(dataPermission); |
| | | } |
| | | try { |
| | | // 执行逻辑 |
| | | return methodInvocation.proceed(); |
| | | } finally { |
| | | // 出栈 |
| | | if (dataPermission != null) { |
| | | DataPermissionContextHolder.remove(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private DataPermission findAnnotation(MethodInvocation methodInvocation) { |
| | | // 1. 从缓存中获取 |
| | | Method method = methodInvocation.getMethod(); |
| | | Object targetObject = methodInvocation.getThis(); |
| | | Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass(); |
| | | MethodClassKey methodClassKey = new MethodClassKey(method, clazz); |
| | | DataPermission dataPermission = dataPermissionCache.get(methodClassKey); |
| | | if (dataPermission != null) { |
| | | return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null; |
| | | } |
| | | |
| | | // 2.1 从方法中获取 |
| | | dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class); |
| | | // 2.2 从类上获取 |
| | | if (dataPermission == null) { |
| | | dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class); |
| | | } |
| | | // 2.3 添加到缓存中 |
| | | dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL); |
| | | return dataPermission; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.aop; |
| | | |
| | | import com.iailab.framework.datapermission.core.annotation.DataPermission; |
| | | import com.alibaba.ttl.TransmittableThreadLocal; |
| | | |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * {@link DataPermission} 注解的 Context 上下文 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DataPermissionContextHolder { |
| | | |
| | | /** |
| | | * 使用 List 的原因,可能存在方法的嵌套调用 |
| | | */ |
| | | private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS = |
| | | TransmittableThreadLocal.withInitial(LinkedList::new); |
| | | |
| | | /** |
| | | * 获得当前的 DataPermission 注解 |
| | | * |
| | | * @return DataPermission 注解 |
| | | */ |
| | | public static DataPermission get() { |
| | | return DATA_PERMISSIONS.get().peekLast(); |
| | | } |
| | | |
| | | /** |
| | | * 入栈 DataPermission 注解 |
| | | * |
| | | * @param dataPermission DataPermission 注解 |
| | | */ |
| | | public static void add(DataPermission dataPermission) { |
| | | DATA_PERMISSIONS.get().addLast(dataPermission); |
| | | } |
| | | |
| | | /** |
| | | * 出栈 DataPermission 注解 |
| | | * |
| | | * @return DataPermission 注解 |
| | | */ |
| | | public static DataPermission remove() { |
| | | DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast(); |
| | | // 无元素时,清空 ThreadLocal |
| | | if (DATA_PERMISSIONS.get().isEmpty()) { |
| | | DATA_PERMISSIONS.remove(); |
| | | } |
| | | return dataPermission; |
| | | } |
| | | |
| | | /** |
| | | * 获得所有 DataPermission |
| | | * |
| | | * @return DataPermission 队列 |
| | | */ |
| | | public static List<DataPermission> getAll() { |
| | | return DATA_PERMISSIONS.get(); |
| | | } |
| | | |
| | | /** |
| | | * 清空上下文 |
| | | * |
| | | * 目前仅仅用于单测 |
| | | */ |
| | | public static void clear() { |
| | | DATA_PERMISSIONS.remove(); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.db; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler; |
| | | import com.iailab.framework.datapermission.core.rule.DataPermissionRule; |
| | | import com.iailab.framework.datapermission.core.rule.DataPermissionRuleFactory; |
| | | import com.iailab.framework.mybatis.core.util.MyBatisUtils; |
| | | import lombok.RequiredArgsConstructor; |
| | | import net.sf.jsqlparser.expression.Expression; |
| | | import net.sf.jsqlparser.expression.operators.conditional.AndExpression; |
| | | import net.sf.jsqlparser.schema.Table; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 基于 {@link DataPermissionRule} 的数据权限处理器 |
| | | * |
| | | * 它的底层,是基于 MyBatis Plus 的 <a href="https://baomidou.com/plugins/data-permission/">数据权限插件</a> |
| | | * 核心原理:它会在 SQL 执行前拦截 SQL 语句,并根据用户权限动态添加权限相关的 SQL 片段。这样,只有用户有权限访问的数据才会被查询出来 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | public class DataPermissionRuleHandler implements MultiDataPermissionHandler { |
| | | |
| | | private final DataPermissionRuleFactory ruleFactory; |
| | | |
| | | @Override |
| | | public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) { |
| | | // 获得 Mapper 对应的数据权限的规则 |
| | | List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId); |
| | | if (CollUtil.isEmpty(rules)) { |
| | | return null; |
| | | } |
| | | |
| | | // 生成条件 |
| | | Expression allExpression = null; |
| | | for (DataPermissionRule rule : rules) { |
| | | // 判断表名是否匹配 |
| | | String tableName = MyBatisUtils.getTableName(table); |
| | | if (!rule.getTableNames().contains(tableName)) { |
| | | continue; |
| | | } |
| | | |
| | | // 单条规则的条件 |
| | | Expression oneExpress = rule.getExpression(tableName, table.getAlias()); |
| | | if (oneExpress == null) { |
| | | continue; |
| | | } |
| | | // 拼接到 allExpression 中 |
| | | allExpression = allExpression == null ? oneExpress |
| | | : new AndExpression(allExpression, oneExpress); |
| | | } |
| | | return allExpression; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.rpc; |
| | | |
| | | import com.iailab.framework.datapermission.core.annotation.DataPermission; |
| | | import com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder; |
| | | import feign.RequestInterceptor; |
| | | import feign.RequestTemplate; |
| | | |
| | | /** |
| | | * DataPermission 的 RequestInterceptor 实现类:Feign 请求时,将 {@link DataPermission} 设置到 header 中,继续透传给被调用的服务 |
| | | * |
| | | * 注意:由于 {@link DataPermission} 不支持序列化和反序列化,所以暂时只能传递它的 enable 属性 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DataPermissionRequestInterceptor implements RequestInterceptor { |
| | | |
| | | public static final String ENABLE_HEADER_NAME = "data-permission-enable"; |
| | | |
| | | @Override |
| | | public void apply(RequestTemplate requestTemplate) { |
| | | DataPermission dataPermission = DataPermissionContextHolder.get(); |
| | | if (dataPermission != null && Boolean.FALSE.equals(dataPermission.enable())) { |
| | | requestTemplate.header(ENABLE_HEADER_NAME, "false"); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.rpc; |
| | | |
| | | import com.iailab.framework.datapermission.core.util.DataPermissionUtils; |
| | | 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.util.Objects; |
| | | |
| | | /** |
| | | * 针对 {@link DataPermissionRequestInterceptor} 的 RPC 调用,设置 {@link com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder} 的上下文 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DataPermissionRpcWebFilter extends OncePerRequestFilter { |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
| | | throws ServletException, IOException { |
| | | String enable = request.getHeader(DataPermissionRequestInterceptor.ENABLE_HEADER_NAME); |
| | | if (Objects.equals(enable, Boolean.FALSE.toString())) { |
| | | DataPermissionUtils.executeIgnore(() -> { |
| | | try { |
| | | chain.doFilter(request, response); |
| | | } catch (IOException | ServletException e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | }); |
| | | } else { |
| | | chain.doFilter(request, response); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.rule; |
| | | |
| | | import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; |
| | | import net.sf.jsqlparser.expression.Alias; |
| | | import net.sf.jsqlparser.expression.Expression; |
| | | |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * 数据权限规则接口 |
| | | * 通过实现接口,自定义数据规则。例如说, |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface DataPermissionRule { |
| | | |
| | | /** |
| | | * 返回需要生效的表名数组 |
| | | * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据 |
| | | * |
| | | * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得 |
| | | * |
| | | * @return 表名数组 |
| | | */ |
| | | Set<String> getTableNames(); |
| | | |
| | | /** |
| | | * 根据表名和别名,生成对应的 WHERE / OR 过滤条件 |
| | | * |
| | | * @param tableName 表名 |
| | | * @param tableAlias 别名,可能为空 |
| | | * @return 过滤条件 Expression 表达式 |
| | | */ |
| | | Expression getExpression(String tableName, Alias tableAlias); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.rule; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * {@link DataPermissionRule} 工厂接口 |
| | | * 作为 {@link DataPermissionRule} 的容器,提供管理能力 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public interface DataPermissionRuleFactory { |
| | | |
| | | /** |
| | | * 获得所有数据权限规则数组 |
| | | * |
| | | * @return 数据权限规则数组 |
| | | */ |
| | | List<DataPermissionRule> getDataPermissionRules(); |
| | | |
| | | /** |
| | | * 获得指定 Mapper 的数据权限规则数组 |
| | | * |
| | | * @param mappedStatementId 指定 Mapper 的编号 |
| | | * @return 数据权限规则数组 |
| | | */ |
| | | List<DataPermissionRule> getDataPermissionRule(String mappedStatementId); |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.rule; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.ArrayUtil; |
| | | import com.iailab.framework.datapermission.core.annotation.DataPermission; |
| | | import com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder; |
| | | import lombok.RequiredArgsConstructor; |
| | | |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * 默认的 DataPermissionRuleFactoryImpl 实现类 |
| | | * 支持通过 {@link DataPermissionContextHolder} 过滤数据权限 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @RequiredArgsConstructor |
| | | public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory { |
| | | |
| | | /** |
| | | * 数据权限规则数组 |
| | | */ |
| | | private final List<DataPermissionRule> rules; |
| | | |
| | | @Override |
| | | public List<DataPermissionRule> getDataPermissionRules() { |
| | | return rules; |
| | | } |
| | | |
| | | @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存 |
| | | public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) { |
| | | // 1. 无数据权限 |
| | | if (CollUtil.isEmpty(rules)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | // 2. 未配置,则默认开启 |
| | | DataPermission dataPermission = DataPermissionContextHolder.get(); |
| | | if (dataPermission == null) { |
| | | return rules; |
| | | } |
| | | // 3. 已配置,但禁用 |
| | | if (!dataPermission.enable()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | // 4. 已配置,只选择部分规则 |
| | | if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) { |
| | | return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass())) |
| | | .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 |
| | | } |
| | | // 5. 已配置,只排除部分规则 |
| | | if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) { |
| | | return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass())) |
| | | .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 |
| | | } |
| | | // 6. 已配置,全部规则 |
| | | return rules; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.rule.dept; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.iailab.framework.common.enums.UserTypeEnum; |
| | | import com.iailab.framework.common.util.collection.CollectionUtils; |
| | | import com.iailab.framework.common.util.json.JsonUtils; |
| | | import com.iailab.framework.datapermission.core.rule.DataPermissionRule; |
| | | import com.iailab.framework.mybatis.core.dataobject.BaseDO; |
| | | import com.iailab.framework.mybatis.core.util.MyBatisUtils; |
| | | 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.iailab.module.system.api.permission.dto.DeptDataPermissionRespDTO; |
| | | import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import net.sf.jsqlparser.expression.*; |
| | | import net.sf.jsqlparser.expression.operators.conditional.OrExpression; |
| | | import net.sf.jsqlparser.expression.operators.relational.EqualsTo; |
| | | import net.sf.jsqlparser.expression.operators.relational.ExpressionList; |
| | | import net.sf.jsqlparser.expression.operators.relational.InExpression; |
| | | import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * 基于部门的 {@link DataPermissionRule} 数据权限规则实现 |
| | | * |
| | | * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 |
| | | * |
| | | * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? |
| | | * 1. 一般情况下,dept_id 不进行修改,则会导致用户看不到之前的数据。【iailab-server 采用该方案】 |
| | | * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 |
| | | * 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 |
| | | * 最终过滤条件是 WHERE dept_id = ? |
| | | * 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号; |
| | | * 最终过滤条件是 WHERE user_id IN (?, ?, ? ...) |
| | | * 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤; |
| | | * 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...) |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AllArgsConstructor |
| | | @Slf4j |
| | | public class DeptDataPermissionRule implements DataPermissionRule { |
| | | |
| | | /** |
| | | * LoginUser 的 Context 缓存 Key |
| | | */ |
| | | protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName(); |
| | | |
| | | private static final String DEPT_COLUMN_NAME = "dept_id"; |
| | | private static final String USER_COLUMN_NAME = "user_id"; |
| | | |
| | | static final Expression EXPRESSION_NULL = new NullValue(); |
| | | |
| | | private final PermissionApi permissionApi; |
| | | |
| | | /** |
| | | * 基于部门的表字段配置 |
| | | * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 |
| | | * |
| | | * key:表名 |
| | | * value:字段名 |
| | | */ |
| | | private final Map<String, String> deptColumns = new HashMap<>(); |
| | | /** |
| | | * 基于用户的表字段配置 |
| | | * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 |
| | | * |
| | | * key:表名 |
| | | * value:字段名 |
| | | */ |
| | | private final Map<String, String> userColumns = new HashMap<>(); |
| | | /** |
| | | * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集 |
| | | */ |
| | | private final Set<String> TABLE_NAMES = new HashSet<>(); |
| | | |
| | | @Override |
| | | public Set<String> getTableNames() { |
| | | return TABLE_NAMES; |
| | | } |
| | | |
| | | @Override |
| | | public Expression getExpression(String tableName, Alias tableAlias) { |
| | | // 只有有登陆用户的情况下,才进行数据权限的处理 |
| | | LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); |
| | | if (loginUser == null) { |
| | | return null; |
| | | } |
| | | // 只有管理员类型的用户,才进行数据权限的处理 |
| | | if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) { |
| | | return null; |
| | | } |
| | | |
| | | // 获得数据权限 |
| | | DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class); |
| | | // 从上下文中拿不到,则调用逻辑进行获取 |
| | | if (deptDataPermission == null) { |
| | | deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId()).getCheckedData(); |
| | | if (deptDataPermission == null) { |
| | | log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser)); |
| | | throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限", |
| | | loginUser.getId(), tableName, tableAlias.getName())); |
| | | } |
| | | // 添加到上下文中,避免重复计算 |
| | | loginUser.setContext(CONTEXT_KEY, deptDataPermission); |
| | | } |
| | | |
| | | // 情况一,如果是 ALL 可查看全部,则无需拼接条件 |
| | | if (deptDataPermission.getAll()) { |
| | | return null; |
| | | } |
| | | |
| | | // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限 |
| | | if (CollUtil.isEmpty(deptDataPermission.getDeptIds()) |
| | | && Boolean.FALSE.equals(deptDataPermission.getSelf())) { |
| | | return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空 |
| | | } |
| | | |
| | | // 情况三,拼接 Dept 和 User 的条件,最后组合 |
| | | Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); |
| | | Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); |
| | | if (deptExpression == null && userExpression == null) { |
| | | // TODO iailab:获得不到条件的时候,暂时不抛出异常,而是不返回数据 |
| | | log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", |
| | | JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission)); |
| | | // throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空", |
| | | // loginUser.getId(), tableName, tableAlias.getName())); |
| | | return EXPRESSION_NULL; |
| | | } |
| | | if (deptExpression == null) { |
| | | return userExpression; |
| | | } |
| | | if (userExpression == null) { |
| | | return deptExpression; |
| | | } |
| | | // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?) |
| | | return new ParenthesedExpressionList(new OrExpression(deptExpression, userExpression)); |
| | | } |
| | | |
| | | private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) { |
| | | // 如果不存在配置,则无需作为条件 |
| | | String columnName = deptColumns.get(tableName); |
| | | if (StrUtil.isEmpty(columnName)) { |
| | | return null; |
| | | } |
| | | // 如果为空,则无条件 |
| | | if (CollUtil.isEmpty(deptIds)) { |
| | | return null; |
| | | } |
| | | // 拼接条件 |
| | | return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), |
| | | // Parenthesis 的目的,是提供 (1,2,3) 的 () 左右括号 |
| | | new ParenthesedExpressionList(new ExpressionList<LongValue>(CollectionUtils.convertList(deptIds, LongValue::new)))); |
| | | } |
| | | |
| | | private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { |
| | | // 如果不查看自己,则无需作为条件 |
| | | if (Boolean.FALSE.equals(self)) { |
| | | return null; |
| | | } |
| | | String columnName = userColumns.get(tableName); |
| | | if (StrUtil.isEmpty(columnName)) { |
| | | return null; |
| | | } |
| | | // 拼接条件 |
| | | return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); |
| | | } |
| | | |
| | | // ==================== 添加配置 ==================== |
| | | |
| | | public void addDeptColumn(Class<? extends BaseDO> entityClass) { |
| | | addDeptColumn(entityClass, DEPT_COLUMN_NAME); |
| | | } |
| | | |
| | | public void addDeptColumn(Class<? extends BaseDO> entityClass, String columnName) { |
| | | String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); |
| | | addDeptColumn(tableName, columnName); |
| | | } |
| | | |
| | | public void addDeptColumn(String tableName, String columnName) { |
| | | deptColumns.put(tableName, columnName); |
| | | TABLE_NAMES.add(tableName); |
| | | } |
| | | |
| | | public void addUserColumn(Class<? extends BaseDO> entityClass) { |
| | | addUserColumn(entityClass, USER_COLUMN_NAME); |
| | | } |
| | | |
| | | public void addUserColumn(Class<? extends BaseDO> entityClass, String columnName) { |
| | | String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); |
| | | addUserColumn(tableName, columnName); |
| | | } |
| | | |
| | | public void addUserColumn(String tableName, String columnName) { |
| | | userColumns.put(tableName, columnName); |
| | | TABLE_NAMES.add(tableName); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.rule.dept; |
| | | |
| | | /** |
| | | * {@link DeptDataPermissionRule} 的自定义配置接口 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @FunctionalInterface |
| | | public interface DeptDataPermissionRuleCustomizer { |
| | | |
| | | /** |
| | | * 自定义该权限规则 |
| | | * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 |
| | | * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则 |
| | | * |
| | | * @param rule 权限规则 |
| | | */ |
| | | void customize(DeptDataPermissionRule rule); |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 基于部门的数据权限规则 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | package com.iailab.framework.datapermission.core.rule.dept; |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.util; |
| | | |
| | | import com.iailab.framework.datapermission.core.annotation.DataPermission; |
| | | import com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder; |
| | | import lombok.SneakyThrows; |
| | | |
| | | import java.util.concurrent.Callable; |
| | | |
| | | /** |
| | | * 数据权限 Util |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DataPermissionUtils { |
| | | |
| | | private static DataPermission DATA_PERMISSION_DISABLE; |
| | | |
| | | @DataPermission(enable = false) |
| | | @SneakyThrows |
| | | private static DataPermission getDisableDataPermissionDisable() { |
| | | if (DATA_PERMISSION_DISABLE == null) { |
| | | DATA_PERMISSION_DISABLE = DataPermissionUtils.class |
| | | .getDeclaredMethod("getDisableDataPermissionDisable") |
| | | .getAnnotation(DataPermission.class); |
| | | } |
| | | return DATA_PERMISSION_DISABLE; |
| | | } |
| | | |
| | | /** |
| | | * 忽略数据权限,执行对应的逻辑 |
| | | * |
| | | * @param runnable 逻辑 |
| | | */ |
| | | public static void executeIgnore(Runnable runnable) { |
| | | DataPermission dataPermission = getDisableDataPermissionDisable(); |
| | | DataPermissionContextHolder.add(dataPermission); |
| | | try { |
| | | // 执行 runnable |
| | | runnable.run(); |
| | | } finally { |
| | | DataPermissionContextHolder.remove(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 忽略数据权限,执行对应的逻辑 |
| | | * |
| | | * @param callable 逻辑 |
| | | * @return 执行结果 |
| | | */ |
| | | @SneakyThrows |
| | | public static <T> T executeIgnore(Callable<T> callable) { |
| | | DataPermission dataPermission = getDisableDataPermissionDisable(); |
| | | DataPermissionContextHolder.add(dataPermission); |
| | | try { |
| | | // 执行 callable |
| | | return callable.call(); |
| | | } finally { |
| | | DataPermissionContextHolder.remove(); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件 |
| | | */ |
| | | package com.iailab.framework.datapermission; |
对比新文件 |
| | |
| | | com.iailab.framework.datapermission.config.IailabDataPermissionAutoConfiguration |
| | | com.iailab.framework.datapermission.config.IailabDeptDataPermissionAutoConfiguration |
| | | com.iailab.framework.datapermission.config.IailabDataPermissionRpcAutoConfiguration |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.aop; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import com.iailab.framework.datapermission.core.annotation.DataPermission; |
| | | import com.iailab.framework.test.core.ut.BaseMockitoUnitTest; |
| | | import org.aopalliance.intercept.MethodInvocation; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.InjectMocks; |
| | | import org.mockito.Mock; |
| | | |
| | | import java.lang.reflect.Method; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | /** |
| | | * {@link DataPermissionAnnotationInterceptor} 的单元测试 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest { |
| | | |
| | | @InjectMocks |
| | | private DataPermissionAnnotationInterceptor interceptor; |
| | | |
| | | @Mock |
| | | private MethodInvocation methodInvocation; |
| | | |
| | | @BeforeEach |
| | | public void setUp() { |
| | | interceptor.getDataPermissionCache().clear(); |
| | | } |
| | | |
| | | @Test // 无 @DataPermission 注解 |
| | | public void testInvoke_none() throws Throwable { |
| | | // 参数 |
| | | mockMethodInvocation(TestNone.class); |
| | | |
| | | // 调用 |
| | | Object result = interceptor.invoke(methodInvocation); |
| | | // 断言 |
| | | assertEquals("none", result); |
| | | assertEquals(1, interceptor.getDataPermissionCache().size()); |
| | | assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); |
| | | } |
| | | |
| | | @Test // 在 Method 上有 @DataPermission 注解 |
| | | public void testInvoke_method() throws Throwable { |
| | | // 参数 |
| | | mockMethodInvocation(TestMethod.class); |
| | | |
| | | // 调用 |
| | | Object result = interceptor.invoke(methodInvocation); |
| | | // 断言 |
| | | assertEquals("method", result); |
| | | assertEquals(1, interceptor.getDataPermissionCache().size()); |
| | | assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); |
| | | } |
| | | |
| | | @Test // 在 Class 上有 @DataPermission 注解 |
| | | public void testInvoke_class() throws Throwable { |
| | | // 参数 |
| | | mockMethodInvocation(TestClass.class); |
| | | |
| | | // 调用 |
| | | Object result = interceptor.invoke(methodInvocation); |
| | | // 断言 |
| | | assertEquals("class", result); |
| | | assertEquals(1, interceptor.getDataPermissionCache().size()); |
| | | assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); |
| | | } |
| | | |
| | | private void mockMethodInvocation(Class<?> clazz) throws Throwable { |
| | | Object targetObject = clazz.newInstance(); |
| | | Method method = targetObject.getClass().getMethod("echo"); |
| | | when(methodInvocation.getThis()).thenReturn(targetObject); |
| | | when(methodInvocation.getMethod()).thenReturn(method); |
| | | when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject)); |
| | | } |
| | | |
| | | static class TestMethod { |
| | | |
| | | @DataPermission(enable = false) |
| | | public String echo() { |
| | | return "method"; |
| | | } |
| | | |
| | | } |
| | | |
| | | @DataPermission(enable = false) |
| | | static class TestClass { |
| | | |
| | | public String echo() { |
| | | return "class"; |
| | | } |
| | | |
| | | } |
| | | |
| | | static class TestNone { |
| | | |
| | | public String echo() { |
| | | return "none"; |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.aop; |
| | | |
| | | import com.iailab.framework.datapermission.core.annotation.DataPermission; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertSame; |
| | | import static org.mockito.Mockito.mock; |
| | | |
| | | /** |
| | | * {@link DataPermissionContextHolder} 的单元测试 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | class DataPermissionContextHolderTest { |
| | | |
| | | @BeforeEach |
| | | public void setUp() { |
| | | DataPermissionContextHolder.clear(); |
| | | } |
| | | |
| | | @Test |
| | | public void testGet() { |
| | | // mock 方法 |
| | | DataPermission dataPermission01 = mock(DataPermission.class); |
| | | DataPermissionContextHolder.add(dataPermission01); |
| | | DataPermission dataPermission02 = mock(DataPermission.class); |
| | | DataPermissionContextHolder.add(dataPermission02); |
| | | |
| | | // 调用 |
| | | DataPermission result = DataPermissionContextHolder.get(); |
| | | // 断言 |
| | | assertSame(result, dataPermission02); |
| | | } |
| | | |
| | | @Test |
| | | public void testPush() { |
| | | // 调用 |
| | | DataPermission dataPermission01 = mock(DataPermission.class); |
| | | DataPermissionContextHolder.add(dataPermission01); |
| | | DataPermission dataPermission02 = mock(DataPermission.class); |
| | | DataPermissionContextHolder.add(dataPermission02); |
| | | // 断言 |
| | | DataPermission first = DataPermissionContextHolder.getAll().get(0); |
| | | DataPermission second = DataPermissionContextHolder.getAll().get(1); |
| | | assertSame(dataPermission01, first); |
| | | assertSame(dataPermission02, second); |
| | | } |
| | | |
| | | @Test |
| | | public void testRemove() { |
| | | // mock 方法 |
| | | DataPermission dataPermission01 = mock(DataPermission.class); |
| | | DataPermissionContextHolder.add(dataPermission01); |
| | | DataPermission dataPermission02 = mock(DataPermission.class); |
| | | DataPermissionContextHolder.add(dataPermission02); |
| | | |
| | | // 调用 |
| | | DataPermission result = DataPermissionContextHolder.remove(); |
| | | // 断言 |
| | | assertSame(result, dataPermission02); |
| | | assertEquals(1, DataPermissionContextHolder.getAll().size()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.db; |
| | | |
| | | import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; |
| | | import com.iailab.framework.datapermission.core.rule.DataPermissionRule; |
| | | import com.iailab.framework.datapermission.core.rule.DataPermissionRuleFactory; |
| | | import com.iailab.framework.mybatis.core.util.MyBatisUtils; |
| | | import com.iailab.framework.test.core.ut.BaseMockitoUnitTest; |
| | | import net.sf.jsqlparser.expression.Alias; |
| | | import net.sf.jsqlparser.expression.Expression; |
| | | import net.sf.jsqlparser.expression.LongValue; |
| | | import net.sf.jsqlparser.expression.Parenthesis; |
| | | import net.sf.jsqlparser.expression.operators.relational.EqualsTo; |
| | | import net.sf.jsqlparser.expression.operators.relational.ExpressionList; |
| | | import net.sf.jsqlparser.expression.operators.relational.InExpression; |
| | | import net.sf.jsqlparser.schema.Column; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.InjectMocks; |
| | | import org.mockito.Mock; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.Set; |
| | | |
| | | import static com.iailab.framework.common.util.collection.SetUtils.asSet; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | /** |
| | | * {@link DataPermissionRuleHandler} 的单元测试 |
| | | * 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试 |
| | | * 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~ |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class DataPermissionRuleHandlerTest extends BaseMockitoUnitTest { |
| | | |
| | | @InjectMocks |
| | | private DataPermissionRuleHandler handler; |
| | | |
| | | @Mock |
| | | private DataPermissionRuleFactory ruleFactory; |
| | | |
| | | private DataPermissionInterceptor interceptor; |
| | | |
| | | @BeforeEach |
| | | public void setUp() { |
| | | interceptor = new DataPermissionInterceptor(handler); |
| | | |
| | | // 租户的数据权限规则 |
| | | DataPermissionRule tenantRule = new DataPermissionRule() { |
| | | |
| | | private static final String COLUMN = "tenant_id"; |
| | | |
| | | @Override |
| | | public Set<String> getTableNames() { |
| | | return asSet("entity", "entity1", "entity2", "entity3", "t1", "t2", "sys_dict_item", // 支持 MyBatis Plus 的单元测试 |
| | | "t_user", "t_role"); // 满足自己的单元测试 |
| | | } |
| | | |
| | | @Override |
| | | public Expression getExpression(String tableName, Alias tableAlias) { |
| | | Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); |
| | | LongValue value = new LongValue(1L); |
| | | return new EqualsTo(column, value); |
| | | } |
| | | |
| | | }; |
| | | // 部门的数据权限规则 |
| | | DataPermissionRule deptRule = new DataPermissionRule() { |
| | | |
| | | private static final String COLUMN = "dept_id"; |
| | | |
| | | @Override |
| | | public Set<String> getTableNames() { |
| | | return asSet("t_user"); // 满足自己的单元测试 |
| | | } |
| | | |
| | | @Override |
| | | public Expression getExpression(String tableName, Alias tableAlias) { |
| | | Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); |
| | | ExpressionList<LongValue> values = new ExpressionList<>(new LongValue(10L), |
| | | new LongValue(20L)); |
| | | return new InExpression(column, new Parenthesis((values))); |
| | | } |
| | | |
| | | }; |
| | | // 设置到上下文 |
| | | when(ruleFactory.getDataPermissionRule(any())).thenReturn(Arrays.asList(tenantRule, deptRule)); |
| | | } |
| | | |
| | | @Test |
| | | void delete() { |
| | | assertSql("delete from entity where id = ?", |
| | | "DELETE FROM entity WHERE id = ? AND entity.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void update() { |
| | | assertSql("update entity set name = ? where id = ?", |
| | | "UPDATE entity SET name = ? WHERE id = ? AND entity.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectSingle() { |
| | | // 单表 |
| | | assertSql("select * from entity where id = ?", |
| | | "SELECT * FROM entity WHERE id = ? AND entity.tenant_id = 1"); |
| | | |
| | | assertSql("select * from entity where id = ? or name = ?", |
| | | "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)", |
| | | "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1"); |
| | | |
| | | /* not */ |
| | | assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)", |
| | | "SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND entity.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectSubSelectIn() { |
| | | /* in */ |
| | | assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)", |
| | | "SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
| | | // 在最前 |
| | | assertSql("SELECT * FROM entity e WHERE e.id IN " + |
| | | "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", |
| | | "SELECT * FROM entity e WHERE e.id IN " + |
| | | "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); |
| | | // 在最后 |
| | | assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + |
| | | "(select e1.id from entity1 e1 where e1.id = ?)", |
| | | "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + |
| | | "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
| | | // 在中间 |
| | | assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + |
| | | "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", |
| | | "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + |
| | | "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectSubSelectEq() { |
| | | /* = */ |
| | | assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)", |
| | | "SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectSubSelectInnerNotEq() { |
| | | /* inner not = */ |
| | | assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))", |
| | | "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)", |
| | | "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectSubSelectExists() { |
| | | /* EXISTS */ |
| | | assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)", |
| | | "SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
| | | |
| | | |
| | | /* NOT EXISTS */ |
| | | assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)", |
| | | "SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectSubSelect() { |
| | | /* >= */ |
| | | assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)", |
| | | "SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
| | | |
| | | |
| | | /* <= */ |
| | | assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)", |
| | | "SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
| | | |
| | | |
| | | /* <> */ |
| | | assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)", |
| | | "SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectFromSelect() { |
| | | assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))", |
| | | "SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)"); |
| | | } |
| | | |
| | | @Test |
| | | void selectBodySubSelect() { |
| | | assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1", |
| | | "SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectLeftJoin() { |
| | | // left join |
| | | assertSql("SELECT * FROM entity e " + |
| | | "left join entity1 e1 on e1.id = e.id " + |
| | | "WHERE e.id = ? OR e.name = ?", |
| | | "SELECT * FROM entity e " + |
| | | "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
| | | "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM entity e " + |
| | | "left join entity1 e1 on e1.id = e.id " + |
| | | "WHERE (e.id = ? OR e.name = ?)", |
| | | "SELECT * FROM entity e " + |
| | | "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
| | | "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM entity e " + |
| | | "left join entity1 e1 on e1.id = e.id " + |
| | | "left join entity2 e2 on e1.id = e2.id", |
| | | "SELECT * FROM entity e " + |
| | | "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
| | | "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " + |
| | | "WHERE e.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectRightJoin() { |
| | | // right join |
| | | assertSql("SELECT * FROM entity e " + |
| | | "right join entity1 e1 on e1.id = e.id", |
| | | "SELECT * FROM entity e " + |
| | | "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + |
| | | "WHERE e1.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM with_as_1 e " + |
| | | "right join entity1 e1 on e1.id = e.id", |
| | | "SELECT * FROM with_as_1 e " + |
| | | "RIGHT JOIN entity1 e1 ON e1.id = e.id " + |
| | | "WHERE e1.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM entity e " + |
| | | "right join entity1 e1 on e1.id = e.id " + |
| | | "WHERE e.id = ? OR e.name = ?", |
| | | "SELECT * FROM entity e " + |
| | | "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + |
| | | "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM entity e " + |
| | | "right join entity1 e1 on e1.id = e.id " + |
| | | "right join entity2 e2 on e1.id = e2.id ", |
| | | "SELECT * FROM entity e " + |
| | | "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + |
| | | "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " + |
| | | "WHERE e2.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectMixJoin() { |
| | | assertSql("SELECT * FROM entity e " + |
| | | "right join entity1 e1 on e1.id = e.id " + |
| | | "left join entity2 e2 on e1.id = e2.id", |
| | | "SELECT * FROM entity e " + |
| | | "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + |
| | | "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " + |
| | | "WHERE e1.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM entity e " + |
| | | "left join entity1 e1 on e1.id = e.id " + |
| | | "right join entity2 e2 on e1.id = e2.id", |
| | | "SELECT * FROM entity e " + |
| | | "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
| | | "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e.tenant_id = 1 " + |
| | | "WHERE e2.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM entity e " + |
| | | "left join entity1 e1 on e1.id = e.id " + |
| | | "inner join entity2 e2 on e1.id = e2.id", |
| | | "SELECT * FROM entity e " + |
| | | "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
| | | "INNER JOIN entity2 e2 ON e1.id = e2.id AND e.tenant_id = 1 AND e2.tenant_id = 1"); |
| | | } |
| | | |
| | | |
| | | @Test |
| | | void selectJoinSubSelect() { |
| | | assertSql("select * from (select * from entity) e1 " + |
| | | "left join entity2 e2 on e1.id = e2.id", |
| | | "SELECT * FROM (SELECT * FROM entity WHERE entity.tenant_id = 1) e1 " + |
| | | "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1"); |
| | | |
| | | assertSql("select * from entity1 e1 " + |
| | | "left join (select * from entity2) e2 " + |
| | | "on e1.id = e2.id", |
| | | "SELECT * FROM entity1 e1 " + |
| | | "LEFT JOIN (SELECT * FROM entity2 WHERE entity2.tenant_id = 1) e2 " + |
| | | "ON e1.id = e2.id " + |
| | | "WHERE e1.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectSubJoin() { |
| | | |
| | | assertSql("select * FROM " + |
| | | "(entity1 e1 right JOIN entity2 e2 ON e1.id = e2.id)", |
| | | "SELECT * FROM " + |
| | | "(entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " + |
| | | "WHERE e2.tenant_id = 1"); |
| | | |
| | | assertSql("select * FROM " + |
| | | "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id)", |
| | | "SELECT * FROM " + |
| | | "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + |
| | | "WHERE e1.tenant_id = 1"); |
| | | |
| | | |
| | | assertSql("select * FROM " + |
| | | "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id) " + |
| | | "right join entity3 e3 on e1.id = e3.id", |
| | | "SELECT * FROM " + |
| | | "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + |
| | | "RIGHT JOIN entity3 e3 ON e1.id = e3.id AND e1.tenant_id = 1 " + |
| | | "WHERE e3.tenant_id = 1"); |
| | | |
| | | |
| | | assertSql("select * FROM entity e " + |
| | | "LEFT JOIN (entity1 e1 right join entity2 e2 ON e1.id = e2.id) " + |
| | | "on e.id = e2.id", |
| | | "SELECT * FROM entity e " + |
| | | "LEFT JOIN (entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " + |
| | | "ON e.id = e2.id AND e2.tenant_id = 1 " + |
| | | "WHERE e.tenant_id = 1"); |
| | | |
| | | assertSql("select * FROM entity e " + |
| | | "LEFT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " + |
| | | "on e.id = e2.id", |
| | | "SELECT * FROM entity e " + |
| | | "LEFT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + |
| | | "ON e.id = e2.id AND e1.tenant_id = 1 " + |
| | | "WHERE e.tenant_id = 1"); |
| | | |
| | | assertSql("select * FROM entity e " + |
| | | "RIGHT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " + |
| | | "on e.id = e2.id", |
| | | "SELECT * FROM entity e " + |
| | | "RIGHT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + |
| | | "ON e.id = e2.id AND e.tenant_id = 1 " + |
| | | "WHERE e1.tenant_id = 1"); |
| | | } |
| | | |
| | | |
| | | @Test |
| | | void selectLeftJoinMultipleTrailingOn() { |
| | | // 多个 on 尾缀的 |
| | | assertSql("SELECT * FROM entity e " + |
| | | "LEFT JOIN entity1 e1 " + |
| | | "LEFT JOIN entity2 e2 ON e2.id = e1.id " + |
| | | "ON e1.id = e.id " + |
| | | "WHERE (e.id = ? OR e.NAME = ?)", |
| | | "SELECT * FROM entity e " + |
| | | "LEFT JOIN entity1 e1 " + |
| | | "LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " + |
| | | "ON e1.id = e.id AND e1.tenant_id = 1 " + |
| | | "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM entity e " + |
| | | "LEFT JOIN entity1 e1 " + |
| | | "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + |
| | | "ON e1.id = e.id " + |
| | | "WHERE (e.id = ? OR e.NAME = ?)", |
| | | "SELECT * FROM entity e " + |
| | | "LEFT JOIN entity1 e1 " + |
| | | "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + |
| | | "ON e1.id = e.id AND e1.tenant_id = 1 " + |
| | | "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | void selectInnerJoin() { |
| | | // inner join |
| | | assertSql("SELECT * FROM entity e " + |
| | | "inner join entity1 e1 on e1.id = e.id " + |
| | | "WHERE e.id = ? OR e.name = ?", |
| | | "SELECT * FROM entity e " + |
| | | "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " + |
| | | "WHERE e.id = ? OR e.name = ?"); |
| | | |
| | | assertSql("SELECT * FROM entity e " + |
| | | "inner join entity1 e1 on e1.id = e.id " + |
| | | "WHERE (e.id = ? OR e.name = ?)", |
| | | "SELECT * FROM entity e " + |
| | | "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " + |
| | | "WHERE (e.id = ? OR e.name = ?)"); |
| | | |
| | | // 隐式内连接 |
| | | assertSql("SELECT * FROM entity,entity1 " + |
| | | "WHERE entity.id = entity1.id", |
| | | "SELECT * FROM entity, entity1 " + |
| | | "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); |
| | | |
| | | // 隐式内连接 |
| | | assertSql("SELECT * FROM entity a, with_as_entity1 b " + |
| | | "WHERE a.id = b.id", |
| | | "SELECT * FROM entity a, with_as_entity1 b " + |
| | | "WHERE a.id = b.id AND a.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM with_as_entity a, with_as_entity1 b " + |
| | | "WHERE a.id = b.id", |
| | | "SELECT * FROM with_as_entity a, with_as_entity1 b " + |
| | | "WHERE a.id = b.id"); |
| | | |
| | | // SubJoin with 隐式内连接 |
| | | assertSql("SELECT * FROM (entity,entity1) " + |
| | | "WHERE entity.id = entity1.id", |
| | | "SELECT * FROM (entity, entity1) " + |
| | | "WHERE entity.id = entity1.id " + |
| | | "AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM ((entity,entity1),entity2) " + |
| | | "WHERE entity.id = entity1.id and entity.id = entity2.id", |
| | | "SELECT * FROM ((entity, entity1), entity2) " + |
| | | "WHERE entity.id = entity1.id AND entity.id = entity2.id " + |
| | | "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1"); |
| | | |
| | | assertSql("SELECT * FROM (entity,(entity1,entity2)) " + |
| | | "WHERE entity.id = entity1.id and entity.id = entity2.id", |
| | | "SELECT * FROM (entity, (entity1, entity2)) " + |
| | | "WHERE entity.id = entity1.id AND entity.id = entity2.id " + |
| | | "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1"); |
| | | |
| | | // 沙雕的括号写法 |
| | | assertSql("SELECT * FROM (((entity,entity1))) " + |
| | | "WHERE entity.id = entity1.id", |
| | | "SELECT * FROM (((entity, entity1))) " + |
| | | "WHERE entity.id = entity1.id " + |
| | | "AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); |
| | | |
| | | } |
| | | |
| | | |
| | | @Test |
| | | void selectWithAs() { |
| | | assertSql("with with_as_A as (select * from entity) select * from with_as_A", |
| | | "WITH with_as_A AS (SELECT * FROM entity WHERE entity.tenant_id = 1) SELECT * FROM with_as_A"); |
| | | } |
| | | |
| | | |
| | | @Test |
| | | void selectIgnoreTable() { |
| | | assertSql(" SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)", |
| | | "SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id AND item.tenant_id = 1 WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)"); |
| | | } |
| | | |
| | | private void assertSql(String sql, String targetSql) { |
| | | assertEquals(targetSql, interceptor.parserSingle(sql, null)); |
| | | } |
| | | |
| | | // ========== 额外的测试 ========== |
| | | |
| | | @Test |
| | | public void testSelectSingle() { |
| | | // 单表 |
| | | assertSql("select * from t_user where id = ?", |
| | | "SELECT * FROM t_user WHERE id = ? AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); |
| | | |
| | | assertSql("select * from t_user where id = ? or name = ?", |
| | | "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); |
| | | |
| | | assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)", |
| | | "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); |
| | | |
| | | /* not */ |
| | | assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)", |
| | | "SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); |
| | | } |
| | | |
| | | @Test |
| | | public void testSelectLeftJoin() { |
| | | // left join |
| | | assertSql("SELECT * FROM t_user e " + |
| | | "left join t_role e1 on e1.id = e.id " + |
| | | "WHERE e.id = ? OR e.name = ?", |
| | | "SELECT * FROM t_user e " + |
| | | "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
| | | "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); |
| | | |
| | | // 条件 e.id = ? OR e.name = ? 带括号 |
| | | assertSql("SELECT * FROM t_user e " + |
| | | "left join t_role e1 on e1.id = e.id " + |
| | | "WHERE (e.id = ? OR e.name = ?)", |
| | | "SELECT * FROM t_user e " + |
| | | "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
| | | "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); |
| | | } |
| | | |
| | | @Test |
| | | public void testSelectRightJoin() { |
| | | // right join |
| | | assertSql("SELECT * FROM t_user e " + |
| | | "right join t_role e1 on e1.id = e.id " + |
| | | "WHERE e.id = ? OR e.name = ?", |
| | | "SELECT * FROM t_user e " + |
| | | "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " + |
| | | "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); |
| | | |
| | | // 条件 e.id = ? OR e.name = ? 带括号 |
| | | assertSql("SELECT * FROM t_user e " + |
| | | "right join t_role e1 on e1.id = e.id " + |
| | | "WHERE (e.id = ? OR e.name = ?)", |
| | | "SELECT * FROM t_user e " + |
| | | "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " + |
| | | "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); |
| | | } |
| | | |
| | | @Test |
| | | public void testSelectInnerJoin() { |
| | | // inner join |
| | | assertSql("SELECT * FROM t_user e " + |
| | | "inner join entity1 e1 on e1.id = e.id " + |
| | | "WHERE e.id = ? OR e.name = ?", |
| | | "SELECT * FROM t_user e " + |
| | | "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " + |
| | | "WHERE e.id = ? OR e.name = ?"); |
| | | |
| | | // 条件 e.id = ? OR e.name = ? 带括号 |
| | | assertSql("SELECT * FROM t_user e " + |
| | | "inner join entity1 e1 on e1.id = e.id " + |
| | | "WHERE (e.id = ? OR e.name = ?)", |
| | | "SELECT * FROM t_user e " + |
| | | "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " + |
| | | "WHERE (e.id = ? OR e.name = ?)"); |
| | | |
| | | // 没有 On 的 inner join |
| | | assertSql("SELECT * FROM entity,entity1 " + |
| | | "WHERE entity.id = entity1.id", |
| | | "SELECT * FROM entity, entity1 " + |
| | | "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.rule; |
| | | |
| | | import com.iailab.framework.datapermission.core.annotation.DataPermission; |
| | | import com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder; |
| | | import com.iailab.framework.test.core.ut.BaseMockitoUnitTest; |
| | | import net.sf.jsqlparser.expression.Alias; |
| | | import net.sf.jsqlparser.expression.Expression; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.InjectMocks; |
| | | import org.mockito.Spy; |
| | | import org.springframework.core.annotation.AnnotationUtils; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | | import static com.iailab.framework.test.core.util.RandomUtils.randomString; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | |
| | | /** |
| | | * {@link DataPermissionRuleFactoryImpl} 单元测试 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest { |
| | | |
| | | @InjectMocks |
| | | private DataPermissionRuleFactoryImpl dataPermissionRuleFactory; |
| | | |
| | | @Spy |
| | | private List<DataPermissionRule> rules = Arrays.asList(new DataPermissionRule01(), |
| | | new DataPermissionRule02()); |
| | | |
| | | @BeforeEach |
| | | public void setUp() { |
| | | DataPermissionContextHolder.clear(); |
| | | } |
| | | |
| | | @Test |
| | | public void testGetDataPermissionRule_02() { |
| | | // 准备参数 |
| | | String mappedStatementId = randomString(); |
| | | |
| | | // 调用 |
| | | List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); |
| | | // 断言 |
| | | assertSame(rules, result); |
| | | } |
| | | |
| | | @Test |
| | | public void testGetDataPermissionRule_03() { |
| | | // 准备参数 |
| | | String mappedStatementId = randomString(); |
| | | // mock 方法 |
| | | DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class)); |
| | | |
| | | // 调用 |
| | | List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); |
| | | // 断言 |
| | | assertTrue(result.isEmpty()); |
| | | } |
| | | |
| | | @Test |
| | | public void testGetDataPermissionRule_04() { |
| | | // 准备参数 |
| | | String mappedStatementId = randomString(); |
| | | // mock 方法 |
| | | DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class)); |
| | | |
| | | // 调用 |
| | | List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); |
| | | // 断言 |
| | | assertEquals(1, result.size()); |
| | | assertEquals(DataPermissionRule01.class, result.get(0).getClass()); |
| | | } |
| | | |
| | | @Test |
| | | public void testGetDataPermissionRule_05() { |
| | | // 准备参数 |
| | | String mappedStatementId = randomString(); |
| | | // mock 方法 |
| | | DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class)); |
| | | |
| | | // 调用 |
| | | List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); |
| | | // 断言 |
| | | assertEquals(1, result.size()); |
| | | assertEquals(DataPermissionRule02.class, result.get(0).getClass()); |
| | | } |
| | | |
| | | @Test |
| | | public void testGetDataPermissionRule_06() { |
| | | // 准备参数 |
| | | String mappedStatementId = randomString(); |
| | | // mock 方法 |
| | | DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class)); |
| | | |
| | | // 调用 |
| | | List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); |
| | | // 断言 |
| | | assertSame(rules, result); |
| | | } |
| | | |
| | | @DataPermission(enable = false) |
| | | static class TestClass03 {} |
| | | |
| | | @DataPermission(includeRules = DataPermissionRule01.class) |
| | | static class TestClass04 {} |
| | | |
| | | @DataPermission(excludeRules = DataPermissionRule01.class) |
| | | static class TestClass05 {} |
| | | |
| | | @DataPermission |
| | | static class TestClass06 {} |
| | | |
| | | static class DataPermissionRule01 implements DataPermissionRule { |
| | | |
| | | @Override |
| | | public Set<String> getTableNames() { |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Expression getExpression(String tableName, Alias tableAlias) { |
| | | return null; |
| | | } |
| | | |
| | | } |
| | | |
| | | static class DataPermissionRule02 implements DataPermissionRule { |
| | | |
| | | @Override |
| | | public Set<String> getTableNames() { |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Expression getExpression(String tableName, Alias tableAlias) { |
| | | return null; |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.rule.dept; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.ReflectUtil; |
| | | import com.iailab.framework.common.enums.UserTypeEnum; |
| | | import com.iailab.framework.common.util.collection.SetUtils; |
| | | import com.iailab.framework.security.core.LoginUser; |
| | | import com.iailab.framework.security.core.util.SecurityFrameworkUtils; |
| | | import com.iailab.framework.test.core.ut.BaseMockitoUnitTest; |
| | | import com.iailab.module.system.api.permission.PermissionApi; |
| | | import com.iailab.module.system.api.permission.dto.DeptDataPermissionRespDTO; |
| | | import net.sf.jsqlparser.expression.Alias; |
| | | import net.sf.jsqlparser.expression.Expression; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.mockito.InjectMocks; |
| | | import org.mockito.Mock; |
| | | import org.mockito.MockedStatic; |
| | | |
| | | import java.util.Map; |
| | | |
| | | import static com.iailab.framework.common.pojo.CommonResult.success; |
| | | import static com.iailab.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL; |
| | | import static com.iailab.framework.test.core.util.RandomUtils.randomPojo; |
| | | import static com.iailab.framework.test.core.util.RandomUtils.randomString; |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.ArgumentMatchers.same; |
| | | import static org.mockito.Mockito.mockStatic; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | /** |
| | | * {@link DeptDataPermissionRule} 的单元测试 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { |
| | | |
| | | @InjectMocks |
| | | private DeptDataPermissionRule rule; |
| | | |
| | | @Mock |
| | | private PermissionApi permissionApi; |
| | | |
| | | @BeforeEach |
| | | @SuppressWarnings("unchecked") |
| | | public void setUp() { |
| | | // 清空 rule |
| | | rule.getTableNames().clear(); |
| | | ((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); |
| | | ((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); |
| | | } |
| | | |
| | | @Test // 无 LoginUser |
| | | public void testGetExpression_noLoginUser() { |
| | | // 准备参数 |
| | | String tableName = randomString(); |
| | | Alias tableAlias = new Alias(randomString()); |
| | | // mock 方法 |
| | | |
| | | // 调用 |
| | | Expression expression = rule.getExpression(tableName, tableAlias); |
| | | // 断言 |
| | | assertNull(expression); |
| | | } |
| | | |
| | | @Test // 无数据权限时 |
| | | public void testGetExpression_noDeptDataPermission() { |
| | | try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
| | | = mockStatic(SecurityFrameworkUtils.class)) { |
| | | // 准备参数 |
| | | String tableName = "t_user"; |
| | | Alias tableAlias = new Alias("u"); |
| | | // mock 方法 |
| | | LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) |
| | | .setUserType(UserTypeEnum.ADMIN.getValue())); |
| | | securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
| | | // mock 方法(permissionApi 返回 null) |
| | | when(permissionApi.getDeptDataPermission(eq(loginUser.getId()))).thenReturn(success(null)); |
| | | |
| | | // 调用 |
| | | NullPointerException exception = assertThrows(NullPointerException.class, |
| | | () -> rule.getExpression(tableName, tableAlias)); |
| | | // 断言 |
| | | assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @Test // 全部数据权限 |
| | | public void testGetExpression_allDeptDataPermission() { |
| | | try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
| | | = mockStatic(SecurityFrameworkUtils.class)) { |
| | | // 准备参数 |
| | | String tableName = "t_user"; |
| | | Alias tableAlias = new Alias("u"); |
| | | // mock 方法(LoginUser) |
| | | LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) |
| | | .setUserType(UserTypeEnum.ADMIN.getValue())); |
| | | securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
| | | // mock 方法(DeptDataPermissionRespDTO) |
| | | DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true); |
| | | when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission)); |
| | | |
| | | // 调用 |
| | | Expression expression = rule.getExpression(tableName, tableAlias); |
| | | // 断言 |
| | | assertNull(expression); |
| | | assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); |
| | | } |
| | | } |
| | | |
| | | @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限 |
| | | public void testGetExpression_noDept_noSelf() { |
| | | try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
| | | = mockStatic(SecurityFrameworkUtils.class)) { |
| | | // 准备参数 |
| | | String tableName = "t_user"; |
| | | Alias tableAlias = new Alias("u"); |
| | | // mock 方法(LoginUser) |
| | | LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) |
| | | .setUserType(UserTypeEnum.ADMIN.getValue())); |
| | | securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
| | | // mock 方法(DeptDataPermissionRespDTO) |
| | | DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); |
| | | when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission)); |
| | | |
| | | // 调用 |
| | | Expression expression = rule.getExpression(tableName, tableAlias); |
| | | // 断言 |
| | | assertEquals("null = null", expression.toString()); |
| | | assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); |
| | | } |
| | | } |
| | | |
| | | @Test // 拼接 Dept 和 User 的条件(字段都不符合) |
| | | public void testGetExpression_noDeptColumn_noSelfColumn() { |
| | | try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
| | | = mockStatic(SecurityFrameworkUtils.class)) { |
| | | // 准备参数 |
| | | String tableName = "t_user"; |
| | | Alias tableAlias = new Alias("u"); |
| | | // mock 方法(LoginUser) |
| | | LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) |
| | | .setUserType(UserTypeEnum.ADMIN.getValue())); |
| | | securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
| | | // mock 方法(DeptDataPermissionRespDTO) |
| | | DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() |
| | | .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true); |
| | | when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission)); |
| | | |
| | | // 调用 |
| | | Expression expression = rule.getExpression(tableName, tableAlias); |
| | | // 断言 |
| | | assertSame(EXPRESSION_NULL, expression); |
| | | assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); |
| | | } |
| | | } |
| | | |
| | | @Test // 拼接 Dept 和 User 的条件(self 符合) |
| | | public void testGetExpression_noDeptColumn_yesSelfColumn() { |
| | | try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
| | | = mockStatic(SecurityFrameworkUtils.class)) { |
| | | // 准备参数 |
| | | String tableName = "t_user"; |
| | | Alias tableAlias = new Alias("u"); |
| | | // mock 方法(LoginUser) |
| | | LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) |
| | | .setUserType(UserTypeEnum.ADMIN.getValue())); |
| | | securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
| | | // mock 方法(DeptDataPermissionRespDTO) |
| | | DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() |
| | | .setSelf(true); |
| | | when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission)); |
| | | // 添加 user 字段配置 |
| | | rule.addUserColumn("t_user", "id"); |
| | | |
| | | // 调用 |
| | | Expression expression = rule.getExpression(tableName, tableAlias); |
| | | // 断言 |
| | | assertEquals("u.id = 1", expression.toString()); |
| | | assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); |
| | | } |
| | | } |
| | | |
| | | @Test // 拼接 Dept 和 User 的条件(dept 符合) |
| | | public void testGetExpression_yesDeptColumn_noSelfColumn() { |
| | | try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
| | | = mockStatic(SecurityFrameworkUtils.class)) { |
| | | // 准备参数 |
| | | String tableName = "t_user"; |
| | | Alias tableAlias = new Alias("u"); |
| | | // mock 方法(LoginUser) |
| | | LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) |
| | | .setUserType(UserTypeEnum.ADMIN.getValue())); |
| | | securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
| | | // mock 方法(DeptDataPermissionRespDTO) |
| | | DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() |
| | | .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)); |
| | | when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission)); |
| | | // 添加 dept 字段配置 |
| | | rule.addDeptColumn("t_user", "dept_id"); |
| | | |
| | | // 调用 |
| | | Expression expression = rule.getExpression(tableName, tableAlias); |
| | | // 断言 |
| | | assertEquals("u.dept_id IN (10, 20)", expression.toString()); |
| | | assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); |
| | | } |
| | | } |
| | | |
| | | @Test // 拼接 Dept 和 User 的条件(dept + self 符合) |
| | | public void testGetExpression_yesDeptColumn_yesSelfColumn() { |
| | | try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
| | | = mockStatic(SecurityFrameworkUtils.class)) { |
| | | // 准备参数 |
| | | String tableName = "t_user"; |
| | | Alias tableAlias = new Alias("u"); |
| | | // mock 方法(LoginUser) |
| | | LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) |
| | | .setUserType(UserTypeEnum.ADMIN.getValue())); |
| | | securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
| | | // mock 方法(DeptDataPermissionRespDTO) |
| | | DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() |
| | | .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true); |
| | | when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission)); |
| | | // 添加 user 字段配置 |
| | | rule.addUserColumn("t_user", "id"); |
| | | // 添加 dept 字段配置 |
| | | rule.addDeptColumn("t_user", "dept_id"); |
| | | |
| | | // 调用 |
| | | Expression expression = rule.getExpression(tableName, tableAlias); |
| | | // 断言 |
| | | assertEquals("(u.dept_id IN (10, 20) OR u.id = 1)", expression.toString()); |
| | | assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.datapermission.core.util; |
| | | |
| | | import com.iailab.framework.datapermission.core.aop.DataPermissionContextHolder; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | |
| | | public class DataPermissionUtilsTest { |
| | | |
| | | @Test |
| | | public void testExecuteIgnore() { |
| | | DataPermissionUtils.executeIgnore(() -> assertFalse(DataPermissionContextHolder.get().enable())); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | <?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-biz-ip</artifactId> |
| | | <packaging>jar</packaging> |
| | | |
| | | <name>${project.artifactId}</name> |
| | | <description>IP 拓展,支持如下功能: |
| | | 1. IP 功能:查询 IP 对应的城市信息 |
| | | 基于 https://gitee.com/lionsoul/ip2region 实现 |
| | | 2. 城市功能:查询城市编码对应的城市信息 |
| | | 基于 https://github.com/modood/Administrative-divisions-of-China 实现 |
| | | </description> |
| | | <url>http://172.16.8.100:8888/summary/iailab-plat.git</url> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- IP地址检索 --> |
| | | <dependency> |
| | | <groupId>org.lionsoul</groupId> |
| | | <artifactId>ip2region</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.projectlombok</groupId> |
| | | <artifactId>lombok</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.slf4j</groupId> |
| | | <artifactId>slf4j-api</artifactId> |
| | | <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
| | | </dependency> |
| | | |
| | | <!-- Test 测试相关 --> |
| | | <dependency> |
| | | <groupId>com.iailab</groupId> |
| | | <artifactId>iailab-common-test</artifactId> |
| | | <scope>test</scope> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
对比新文件 |
| | |
| | | package com.iailab.framework.ip.core; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonManagedReference; |
| | | import com.iailab.framework.ip.core.enums.AreaTypeEnum; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Data; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.ToString; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 区域节点,包括国家、省份、城市、地区等信息 |
| | | * |
| | | * 数据可见 resources/area.csv 文件 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Data |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @ToString(exclude = {"parent"}) |
| | | public class Area { |
| | | |
| | | /** |
| | | * 编号 - 全球,即根目录 |
| | | */ |
| | | public static final Integer ID_GLOBAL = 0; |
| | | /** |
| | | * 编号 - 中国 |
| | | */ |
| | | public static final Integer ID_CHINA = 1; |
| | | |
| | | /** |
| | | * 编号 |
| | | */ |
| | | private Integer id; |
| | | /** |
| | | * 名字 |
| | | */ |
| | | private String name; |
| | | /** |
| | | * 类型 |
| | | * |
| | | * 枚举 {@link AreaTypeEnum} |
| | | */ |
| | | private Integer type; |
| | | |
| | | /** |
| | | * 父节点 |
| | | */ |
| | | @JsonManagedReference |
| | | private Area parent; |
| | | /** |
| | | * 子节点 |
| | | */ |
| | | @JsonManagedReference |
| | | private List<Area> children; |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ip.core.enums; |
| | | |
| | | import com.iailab.framework.common.core.IntArrayValuable; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Getter; |
| | | |
| | | import java.util.Arrays; |
| | | |
| | | /** |
| | | * 区域类型枚举 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @AllArgsConstructor |
| | | @Getter |
| | | public enum AreaTypeEnum implements IntArrayValuable { |
| | | |
| | | COUNTRY(1, "国家"), |
| | | PROVINCE(2, "省份"), |
| | | CITY(3, "城市"), |
| | | DISTRICT(4, "地区"), // 县、镇、区等 |
| | | ; |
| | | |
| | | public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AreaTypeEnum::getType).toArray(); |
| | | |
| | | /** |
| | | * 类型 |
| | | */ |
| | | private final Integer type; |
| | | /** |
| | | * 名字 |
| | | */ |
| | | private final String name; |
| | | |
| | | @Override |
| | | public int[] array() { |
| | | return ARRAYS; |
| | | } |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ip.core.utils; |
| | | |
| | | import cn.hutool.core.io.resource.ResourceUtil; |
| | | import cn.hutool.core.lang.Assert; |
| | | import cn.hutool.core.text.csv.CsvRow; |
| | | import cn.hutool.core.text.csv.CsvUtil; |
| | | import com.iailab.framework.common.util.object.ObjectUtils; |
| | | import com.iailab.framework.ip.core.Area; |
| | | import com.iailab.framework.ip.core.enums.AreaTypeEnum; |
| | | import lombok.NonNull; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.function.Function; |
| | | |
| | | import static com.iailab.framework.common.util.collection.CollectionUtils.convertList; |
| | | import static com.iailab.framework.common.util.collection.CollectionUtils.findFirst; |
| | | |
| | | /** |
| | | * 区域工具类 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | @Slf4j |
| | | public class AreaUtils { |
| | | |
| | | /** |
| | | * 初始化 SEARCHER |
| | | */ |
| | | @SuppressWarnings("InstantiationOfUtilityClass") |
| | | private final static AreaUtils INSTANCE = new AreaUtils(); |
| | | |
| | | /** |
| | | * Area 内存缓存,提升访问速度 |
| | | */ |
| | | private static Map<Integer, Area> areas; |
| | | |
| | | private AreaUtils() { |
| | | long now = System.currentTimeMillis(); |
| | | areas = new HashMap<>(); |
| | | areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, |
| | | null, new ArrayList<>())); |
| | | // 从 csv 中加载数据 |
| | | List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows(); |
| | | rows.remove(0); // 删除 header |
| | | for (CsvRow row : rows) { |
| | | // 创建 Area 对象 |
| | | Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)), |
| | | null, new ArrayList<>()); |
| | | // 添加到 areas 中 |
| | | areas.put(area.getId(), area); |
| | | } |
| | | |
| | | // 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取 |
| | | for (CsvRow row : rows) { |
| | | Area area = areas.get(Integer.valueOf(row.get(0))); // 自己 |
| | | Area parent = areas.get(Integer.valueOf(row.get(3))); // 父 |
| | | Assert.isTrue(area != parent, "{}:父子节点相同", area.getName()); |
| | | area.setParent(parent); |
| | | parent.getChildren().add(area); |
| | | } |
| | | log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); |
| | | } |
| | | |
| | | /** |
| | | * 获得指定编号对应的区域 |
| | | * |
| | | * @param id 区域编号 |
| | | * @return 区域 |
| | | */ |
| | | public static Area getArea(Integer id) { |
| | | return areas.get(id); |
| | | } |
| | | |
| | | /** |
| | | * 获得指定区域对应的编号 |
| | | * |
| | | * @param pathStr 区域路径,例如说:河南省/石家庄市/新华区 |
| | | * @return 区域 |
| | | */ |
| | | public static Area parseArea(String pathStr) { |
| | | String[] paths = pathStr.split("/"); |
| | | Area area = null; |
| | | for (String path : paths) { |
| | | if (area == null) { |
| | | area = findFirst(areas.values(), item -> item.getName().equals(path)); |
| | | } else { |
| | | area = findFirst(area.getChildren(), item -> item.getName().equals(path)); |
| | | } |
| | | } |
| | | return area; |
| | | } |
| | | |
| | | /** |
| | | * 获取所有节点的全路径名称如:河南省/石家庄市/新华区 |
| | | * |
| | | * @param areas 地区树 |
| | | * @return 所有节点的全路径名称 |
| | | */ |
| | | public static List<String> getAreaNodePathList(List<Area> areas) { |
| | | List<String> paths = new ArrayList<>(); |
| | | areas.forEach(area -> getAreaNodePathList(area, "", paths)); |
| | | return paths; |
| | | } |
| | | |
| | | /** |
| | | * 构建一棵树的所有节点的全路径名称,并将其存储为 "祖先/父级/子级" 的形式 |
| | | * |
| | | * @param node 父节点 |
| | | * @param path 全路径名称 |
| | | * @param paths 全路径名称列表,省份/城市/地区 |
| | | */ |
| | | private static void getAreaNodePathList(Area node, String path, List<String> paths) { |
| | | if (node == null) { |
| | | return; |
| | | } |
| | | // 构建当前节点的路径 |
| | | String currentPath = path.isEmpty() ? node.getName() : path + "/" + node.getName(); |
| | | paths.add(currentPath); |
| | | // 递归遍历子节点 |
| | | for (Area child : node.getChildren()) { |
| | | getAreaNodePathList(child, currentPath, paths); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 格式化区域 |
| | | * |
| | | * @param id 区域编号 |
| | | * @return 格式化后的区域 |
| | | */ |
| | | public static String format(Integer id) { |
| | | return format(id, " "); |
| | | } |
| | | |
| | | /** |
| | | * 格式化区域 |
| | | * |
| | | * 例如说: |
| | | * 1. id = “静安区”时:上海 上海市 静安区 |
| | | * 2. id = “上海市”时:上海 上海市 |
| | | * 3. id = “上海”时:上海 |
| | | * 4. id = “美国”时:美国 |
| | | * 当区域在中国时,默认不显示中国 |
| | | * |
| | | * @param id 区域编号 |
| | | * @param separator 分隔符 |
| | | * @return 格式化后的区域 |
| | | */ |
| | | public static String format(Integer id, String separator) { |
| | | // 获得区域 |
| | | Area area = areas.get(id); |
| | | if (area == null) { |
| | | return null; |
| | | } |
| | | |
| | | // 格式化 |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (int i = 0; i < AreaTypeEnum.values().length; i++) { // 避免死循环 |
| | | sb.insert(0, area.getName()); |
| | | // “递归”父节点 |
| | | area = area.getParent(); |
| | | if (area == null |
| | | || ObjectUtils.equalsAny(area.getId(), Area.ID_GLOBAL, Area.ID_CHINA)) { // 跳过父节点为中国的情况 |
| | | break; |
| | | } |
| | | sb.insert(0, separator); |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** |
| | | * 获取指定类型的区域列表 |
| | | * |
| | | * @param type 区域类型 |
| | | * @param func 转换函数 |
| | | * @param <T> 结果类型 |
| | | * @return 区域列表 |
| | | */ |
| | | public static <T> List<T> getByType(AreaTypeEnum type, Function<Area, T> func) { |
| | | return convertList(areas.values(), func, area -> type.getType().equals(area.getType())); |
| | | } |
| | | |
| | | /** |
| | | * 根据区域编号、上级区域类型,获取上级区域编号 |
| | | * |
| | | * @param id 区域编号 |
| | | * @param type 区域类型 |
| | | * @return 上级区域编号 |
| | | */ |
| | | public static Integer getParentIdByType(Integer id, @NonNull AreaTypeEnum type) { |
| | | for (int i = 0; i < Byte.MAX_VALUE; i++) { |
| | | Area area = AreaUtils.getArea(id); |
| | | if (area == null) { |
| | | return null; |
| | | } |
| | | // 情况一:匹配到,返回它 |
| | | if (type.getType().equals(area.getType())) { |
| | | return area.getId(); |
| | | } |
| | | // 情况二:找到根节点,返回空 |
| | | if (area.getParent() == null || area.getParent().getId() == null) { |
| | | return null; |
| | | } |
| | | // 其它:继续向上查找 |
| | | id = area.getParent().getId(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ip.core.utils; |
| | | |
| | | import cn.hutool.core.io.resource.ResourceUtil; |
| | | import com.iailab.framework.ip.core.Area; |
| | | import lombok.SneakyThrows; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.lionsoul.ip2region.xdb.Searcher; |
| | | |
| | | import java.io.IOException; |
| | | |
| | | /** |
| | | * IP 工具类 |
| | | * |
| | | * IP 数据源来自 ip2region.xdb 精简版,基于 <a href="https://gitee.com/zhijiantianya/ip2region"/> 项目 |
| | | * |
| | | * @author wanglhup |
| | | */ |
| | | @Slf4j |
| | | public class IPUtils { |
| | | |
| | | /** |
| | | * 初始化 SEARCHER |
| | | */ |
| | | @SuppressWarnings("InstantiationOfUtilityClass") |
| | | private final static IPUtils INSTANCE = new IPUtils(); |
| | | |
| | | /** |
| | | * IP 查询器,启动加载到内存中 |
| | | */ |
| | | private static Searcher SEARCHER; |
| | | |
| | | /** |
| | | * 私有化构造 |
| | | */ |
| | | private IPUtils() { |
| | | try { |
| | | long now = System.currentTimeMillis(); |
| | | byte[] bytes = ResourceUtil.readBytes("ip2region.xdb"); |
| | | SEARCHER = Searcher.newWithBuffer(bytes); |
| | | log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); |
| | | } catch (IOException e) { |
| | | log.error("启动加载 IPUtils 失败", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 查询 IP 对应的地区编号 |
| | | * |
| | | * @param ip IP 地址,格式为 127.0.0.1 |
| | | * @return 地区id |
| | | */ |
| | | @SneakyThrows |
| | | public static Integer getAreaId(String ip) { |
| | | return Integer.parseInt(SEARCHER.search(ip.trim())); |
| | | } |
| | | |
| | | /** |
| | | * 查询 IP 对应的地区编号 |
| | | * |
| | | * @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回 |
| | | * @return 地区编号 |
| | | */ |
| | | @SneakyThrows |
| | | public static Integer getAreaId(long ip) { |
| | | return Integer.parseInt(SEARCHER.search(ip)); |
| | | } |
| | | |
| | | /** |
| | | * 查询 IP 对应的地区 |
| | | * |
| | | * @param ip IP 地址,格式为 127.0.0.1 |
| | | * @return 地区 |
| | | */ |
| | | public static Area getArea(String ip) { |
| | | return AreaUtils.getArea(getAreaId(ip)); |
| | | } |
| | | |
| | | /** |
| | | * 查询 IP 对应的地区 |
| | | * |
| | | * @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回 |
| | | * @return 地区 |
| | | */ |
| | | public static Area getArea(long ip) { |
| | | return AreaUtils.getArea(getAreaId(ip)); |
| | | } |
| | | } |
对比新文件 |
| | |
| | | /** |
| | | * IP 拓展,支持如下功能: |
| | | * |
| | | * 1. IP 功能:查询 IP 对应的城市信息 |
| | | * 基于 https://gitee.com/lionsoul/ip2region 实现 |
| | | * 2. 城市功能:查询城市编码对应的城市信息 |
| | | * 基于 https://github.com/modood/Administrative-divisions-of-China 实现 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | package com.iailab.framework.ip; |
对比新文件 |
| | |
| | | id,name,type,parentId |
| | | 1,中国,1,0 |
| | | 2,蒙古,1,0 |
| | | 3,朝鲜,1,0 |
| | | 4,韩国,1,0 |
| | | 5,日本,1,0 |
| | | 6,菲律宾,1,0 |
| | | 7,越南,1,0 |
| | | 8,老挝,1,0 |
| | | 9,柬埔寨,1,0 |
| | | 10,缅甸,1,0 |
| | | 11,泰国,1,0 |
| | | 12,马来西亚,1,0 |
| | | 13,文莱,1,0 |
| | | 14,新加坡,1,0 |
| | | 15,印度尼西亚,1,0 |
| | | 16,东帝汶,1,0 |
| | | 17,尼泊尔,1,0 |
| | | 18,不丹,1,0 |
| | | 19,孟加拉国,1,0 |
| | | 20,印度,1,0 |
| | | 21,巴基斯坦,1,0 |
| | | 22,斯里兰卡,1,0 |
| | | 23,马尔代夫,1,0 |
| | | 24,哈萨克斯坦,1,0 |
| | | 25,吉尔吉斯斯坦,1,0 |
| | | 26,塔吉克斯坦,1,0 |
| | | 27,乌兹别克斯坦,1,0 |
| | | 28,土库曼斯坦,1,0 |
| | | 29,阿富汗,1,0 |
| | | 30,伊拉克,1,0 |
| | | 31,伊朗,1,0 |
| | | 32,叙利亚,1,0 |
| | | 33,约旦,1,0 |
| | | 34,黎巴嫩,1,0 |
| | | 35,以色列,1,0 |
| | | 36,巴勒斯坦,1,0 |
| | | 37,沙特阿拉伯,1,0 |
| | | 38,巴林,1,0 |
| | | 39,卡塔尔,1,0 |
| | | 40,科威特,1,0 |
| | | 41,阿拉伯联合酋长国,1,0 |
| | | 42,阿曼,1,0 |
| | | 43,也门,1,0 |
| | | 44,格鲁吉亚,1,0 |
| | | 45,亚美尼亚,1,0 |
| | | 46,阿塞拜疆,1,0 |
| | | 47,土耳其,1,0 |
| | | 48,塞浦路斯,1,0 |
| | | 49,芬兰,1,0 |
| | | 50,瑞典,1,0 |
| | | 51,挪威,1,0 |
| | | 52,冰岛,1,0 |
| | | 53,丹麦,1,0 |
| | | 54,爱沙尼亚,1,0 |
| | | 55,拉脱维亚,1,0 |
| | | 56,立陶宛,1,0 |
| | | 57,白俄罗斯,1,0 |
| | | 58,俄罗斯,1,0 |
| | | 59,乌克兰,1,0 |
| | | 60,摩尔多瓦,1,0 |
| | | 61,波兰,1,0 |
| | | 62,捷克,1,0 |
| | | 63,斯洛伐克,1,0 |
| | | 64,匈牙利,1,0 |
| | | 65,德国,1,0 |
| | | 66,奥地利,1,0 |
| | | 67,瑞士,1,0 |
| | | 68,列支敦士登,1,0 |
| | | 69,英国,1,0 |
| | | 70,爱尔兰,1,0 |
| | | 71,荷兰,1,0 |
| | | 72,比利时,1,0 |
| | | 73,卢森堡,1,0 |
| | | 74,法国,1,0 |
| | | 75,摩纳哥,1,0 |
| | | 76,罗马尼亚,1,0 |
| | | 77,保加利亚,1,0 |
| | | 78,塞尔维亚,1,0 |
| | | 79,马其顿,1,0 |
| | | 80,阿尔巴尼亚,1,0 |
| | | 81,希腊,1,0 |
| | | 82,斯洛文尼亚,1,0 |
| | | 83,克罗地亚,1,0 |
| | | 84,波斯尼亚和墨塞哥维那,1,0 |
| | | 85,意大利,1,0 |
| | | 86,梵蒂冈,1,0 |
| | | 87,圣马力诺,1,0 |
| | | 88,马耳他,1,0 |
| | | 89,西班牙,1,0 |
| | | 90,葡萄牙,1,0 |
| | | 91,安道尔共和国,1,0 |
| | | 92,埃及,1,0 |
| | | 93,利比亚,1,0 |
| | | 94,苏丹,1,0 |
| | | 95,突尼斯,1,0 |
| | | 96,阿尔及利亚,1,0 |
| | | 97,摩洛哥,1,0 |
| | | 98,亚速尔群岛,1,0 |
| | | 99,马德拉群岛,1,0 |
| | | 100,埃塞俄比亚,1,0 |
| | | 101,厄立特里亚,1,0 |
| | | 102,索马里,1,0 |
| | | 103,吉布提,1,0 |
| | | 104,肯尼亚,1,0 |
| | | 105,坦桑尼亚,1,0 |
| | | 106,乌干达,1,0 |
| | | 107,卢旺达,1,0 |
| | | 108,布隆迪,1,0 |
| | | 109,塞舌尔,1,0 |
| | | 110,圣多美及普林西比,1,0 |
| | | 111,塞内加尔,1,0 |
| | | 112,冈比亚,1,0 |
| | | 113,马里,1,0 |
| | | 114,布基纳法索,1,0 |
| | | 115,几内亚,1,0 |
| | | 116,几内亚比绍,1,0 |
| | | 117,佛得角,1,0 |
| | | 118,塞拉利昂,1,0 |
| | | 119,利比里亚,1,0 |
| | | 120,科特迪瓦,1,0 |
| | | 121,加纳,1,0 |
| | | 122,多哥,1,0 |
| | | 123,贝宁,1,0 |
| | | 124,尼日尔,1,0 |
| | | 125,加那利群岛,1,0 |
| | | 126,赞比亚,1,0 |
| | | 127,安哥拉,1,0 |
| | | 128,津巴布韦,1,0 |
| | | 129,马拉维,1,0 |
| | | 130,莫桑比克,1,0 |
| | | 131,博茨瓦纳,1,0 |
| | | 132,纳米比亚,1,0 |
| | | 133,南非,1,0 |
| | | 134,斯威士兰,1,0 |
| | | 135,莱索托,1,0 |
| | | 136,马达加斯加,1,0 |
| | | 137,科摩罗,1,0 |
| | | 138,毛里求斯,1,0 |
| | | 139,留尼旺,1,0 |
| | | 140,圣赫勒拿,1,0 |
| | | 141,澳大利亚,1,0 |
| | | 142,新西兰,1,0 |
| | | 143,巴布亚新几内亚,1,0 |
| | | 144,所罗门群岛,1,0 |
| | | 145,瓦努阿图共和国,1,0 |
| | | 146,密克罗尼西亚,1,0 |
| | | 147,马绍尔群岛,1,0 |
| | | 148,帕劳,1,0 |
| | | 149,瑙鲁,1,0 |
| | | 150,基里巴斯,1,0 |
| | | 151,图瓦卢,1,0 |
| | | 152,萨摩亚,1,0 |
| | | 153,斐济,1,0 |
| | | 154,汤加,1,0 |
| | | 155,库克群岛,1,0 |
| | | 156,关岛,1,0 |
| | | 157,新喀里多尼亚,1,0 |
| | | 158,法属波利尼西亚,1,0 |
| | | 159,皮特凯恩岛,1,0 |
| | | 160,瓦利斯与富图纳,1,0 |
| | | 161,纽埃,1,0 |
| | | 162,托克劳,1,0 |
| | | 163,美属萨摩亚,1,0 |
| | | 164,北马里亚纳,1,0 |
| | | 165,加拿大,1,0 |
| | | 166,美国,1,0 |
| | | 167,墨西哥,1,0 |
| | | 168,格陵兰,1,0 |
| | | 169,危地马拉,1,0 |
| | | 170,伯利兹,1,0 |
| | | 171,萨尔瓦多,1,0 |
| | | 172,洪都拉斯,1,0 |
| | | 173,尼加拉瓜,1,0 |
| | | 174,哥斯达黎加,1,0 |
| | | 175,巴拿马,1,0 |
| | | 176,巴哈马,1,0 |
| | | 177,古巴,1,0 |
| | | 178,牙买加,1,0 |
| | | 179,海地,1,0 |
| | | 180,多米尼加共和国,1,0 |
| | | 181,安提瓜和巴布达,1,0 |
| | | 182,圣基茨和尼维斯,1,0 |
| | | 183,多米尼克,1,0 |
| | | 184,圣卢西亚,1,0 |
| | | 185,圣文森特和格林纳丁斯,1,0 |
| | | 186,格林纳达,1,0 |
| | | 187,巴巴多斯,1,0 |
| | | 188,特立尼达和多巴哥,1,0 |
| | | 189,波多黎各,1,0 |
| | | 190,英属维尔京群岛,1,0 |
| | | 191,美属维尔京群岛,1,0 |
| | | 192,安圭拉,1,0 |
| | | 193,蒙特塞拉特岛,1,0 |
| | | 194,瓜德罗普,1,0 |
| | | 195,马提尼克,1,0 |
| | | 196,荷属安的列斯,1,0 |
| | | 197,阿鲁巴,1,0 |
| | | 198,特克斯和凯科斯群岛,1,0 |
| | | 199,开曼群岛,1,0 |
| | | 200,百慕大,1,0 |
| | | 201,哥伦比亚,1,0 |
| | | 202,委内瑞拉,1,0 |
| | | 203,圭亚那,1,0 |
| | | 204,法属圭亚那,1,0 |
| | | 205,苏里南,1,0 |
| | | 206,厄瓜多尔,1,0 |
| | | 207,秘鲁,1,0 |
| | | 208,玻利维亚,1,0 |
| | | 209,巴西,1,0 |
| | | 210,智利,1,0 |
| | | 211,阿根廷,1,0 |
| | | 212,乌拉圭,1,0 |
| | | 213,巴拉圭,1,0 |
| | | 214,波黑,1,0 |
| | | 215,直布罗陀,1,0 |
| | | 216,新喀里多尼亚群岛,1,0 |
| | | 217,瓦利斯和富图纳群岛,1,0 |
| | | 218,泽西岛,1,0 |
| | | 219,黑山,1,0 |
| | | 220,英属马恩岛,1,0 |
| | | 221,尼日利亚,1,0 |
| | | 222,喀麦隆,1,0 |
| | | 223,加蓬,1,0 |
| | | 224,乍得,1,0 |
| | | 225,刚果共和国,1,0 |
| | | 226,中非共和国,1,0 |
| | | 227,南苏丹,1,0 |
| | | 228,赤道几内亚,1,0 |
| | | 229,毛里塔尼亚,1,0 |
| | | 230,刚果民主共和国,1,0 |
| | | 231,留尼汪岛,1,0 |
| | | 232,格陵兰岛,1,0 |
| | | 233,法罗群岛,1,0 |
| | | 234,根西岛,1,0 |
| | | 235,百慕大群岛,1,0 |
| | | 236,圣皮埃尔和密克隆群岛,1,0 |
| | | 237,法属圣马丁,1,0 |
| | | 238,奥兰群岛,1,0 |
| | | 239,北马里亚纳群岛,1,0 |
| | | 240,库拉索,1,0 |
| | | 241,博内尔岛,1,0 |
| | | 242,圣马丁岛,1,0 |
| | | 243,圣巴泰勒米岛,1,0 |
| | | 244,福克兰群岛,1,0 |
| | | 245,圣多美和普林西比,1,0 |
| | | 246,英属印度洋领地,1,0 |
| | | 247,东萨摩亚,1,0 |
| | | 248,诺福克岛,1,0 |
| | | 110000,北京,2,1 |
| | | 120000,天津,2,1 |
| | | 130000,河北省,2,1 |
| | | 140000,山西省,2,1 |
| | | 150000,内蒙古自治区,2,1 |
| | | 210000,辽宁省,2,1 |
| | | 220000,吉林省,2,1 |
| | | 230000,黑龙江省,2,1 |
| | | 310000,上海,2,1 |
| | | 320000,江苏省,2,1 |
| | | 330000,浙江省,2,1 |
| | | 340000,安徽省,2,1 |
| | | 350000,福建省,2,1 |
| | | 360000,江西省,2,1 |
| | | 370000,山东省,2,1 |
| | | 410000,河南省,2,1 |
| | | 420000,湖北省,2,1 |
| | | 430000,湖南省,2,1 |
| | | 440000,广东省,2,1 |
| | | 450000,广西壮族自治区,2,1 |
| | | 460000,海南省,2,1 |
| | | 500000,重庆,2,1 |
| | | 510000,四川省,2,1 |
| | | 520000,贵州省,2,1 |
| | | 530000,云南省,2,1 |
| | | 540000,西藏自治区,2,1 |
| | | 610000,陕西省,2,1 |
| | | 620000,甘肃省,2,1 |
| | | 630000,青海省,2,1 |
| | | 640000,宁夏回族自治区,2,1 |
| | | 650000,新疆维吾尔自治区,2,1 |
| | | 110100,北京市,3,110000 |
| | | 120100,天津市,3,120000 |
| | | 130100,石家庄市,3,130000 |
| | | 130200,唐山市,3,130000 |
| | | 130300,秦皇岛市,3,130000 |
| | | 130400,邯郸市,3,130000 |
| | | 130500,邢台市,3,130000 |
| | | 130600,保定市,3,130000 |
| | | 130700,张家口市,3,130000 |
| | | 130800,承德市,3,130000 |
| | | 130900,沧州市,3,130000 |
| | | 131000,廊坊市,3,130000 |
| | | 131100,衡水市,3,130000 |
| | | 140100,太原市,3,140000 |
| | | 140200,大同市,3,140000 |
| | | 140300,阳泉市,3,140000 |
| | | 140400,长治市,3,140000 |
| | | 140500,晋城市,3,140000 |
| | | 140600,朔州市,3,140000 |
| | | 140700,晋中市,3,140000 |
| | | 140800,运城市,3,140000 |
| | | 140900,忻州市,3,140000 |
| | | 141000,临汾市,3,140000 |
| | | 141100,吕梁市,3,140000 |
| | | 150100,呼和浩特市,3,150000 |
| | | 150200,包头市,3,150000 |
| | | 150300,乌海市,3,150000 |
| | | 150400,赤峰市,3,150000 |
| | | 150500,通辽市,3,150000 |
| | | 150600,鄂尔多斯市,3,150000 |
| | | 150700,呼伦贝尔市,3,150000 |
| | | 150800,巴彦淖尔市,3,150000 |
| | | 150900,乌兰察布市,3,150000 |
| | | 152200,兴安盟,3,150000 |
| | | 152500,锡林郭勒盟,3,150000 |
| | | 152900,阿拉善盟,3,150000 |
| | | 210100,沈阳市,3,210000 |
| | | 210200,大连市,3,210000 |
| | | 210300,鞍山市,3,210000 |
| | | 210400,抚顺市,3,210000 |
| | | 210500,本溪市,3,210000 |
| | | 210600,丹东市,3,210000 |
| | | 210700,锦州市,3,210000 |
| | | 210800,营口市,3,210000 |
| | | 210900,阜新市,3,210000 |
| | | 211000,辽阳市,3,210000 |
| | | 211100,盘锦市,3,210000 |
| | | 211200,铁岭市,3,210000 |
| | | 211300,朝阳市,3,210000 |
| | | 211400,葫芦岛市,3,210000 |
| | | 220100,长春市,3,220000 |
| | | 220200,吉林市,3,220000 |
| | | 220300,四平市,3,220000 |
| | | 220400,辽源市,3,220000 |
| | | 220500,通化市,3,220000 |
| | | 220600,白山市,3,220000 |
| | | 220700,松原市,3,220000 |
| | | 220800,白城市,3,220000 |
| | | 222400,延边朝鲜族自治州,3,220000 |
| | | 230100,哈尔滨市,3,230000 |
| | | 230200,齐齐哈尔市,3,230000 |
| | | 230300,鸡西市,3,230000 |
| | | 230400,鹤岗市,3,230000 |
| | | 230500,双鸭山市,3,230000 |
| | | 230600,大庆市,3,230000 |
| | | 230700,伊春市,3,230000 |
| | | 230800,佳木斯市,3,230000 |
| | | 230900,七台河市,3,230000 |
| | | 231000,牡丹江市,3,230000 |
| | | 231100,黑河市,3,230000 |
| | | 231200,绥化市,3,230000 |
| | | 232700,大兴安岭地区,3,230000 |
| | | 310100,上海市,3,310000 |
| | | 320100,南京市,3,320000 |
| | | 320200,无锡市,3,320000 |
| | | 320300,徐州市,3,320000 |
| | | 320400,常州市,3,320000 |
| | | 320500,苏州市,3,320000 |
| | | 320600,南通市,3,320000 |
| | | 320700,连云港市,3,320000 |
| | | 320800,淮安市,3,320000 |
| | | 320900,盐城市,3,320000 |
| | | 321000,扬州市,3,320000 |
| | | 321100,镇江市,3,320000 |
| | | 321200,泰州市,3,320000 |
| | | 321300,宿迁市,3,320000 |
| | | 330100,杭州市,3,330000 |
| | | 330200,宁波市,3,330000 |
| | | 330300,温州市,3,330000 |
| | | 330400,嘉兴市,3,330000 |
| | | 330500,湖州市,3,330000 |
| | | 330600,绍兴市,3,330000 |
| | | 330700,金华市,3,330000 |
| | | 330800,衢州市,3,330000 |
| | | 330900,舟山市,3,330000 |
| | | 331000,台州市,3,330000 |
| | | 331100,丽水市,3,330000 |
| | | 340100,合肥市,3,340000 |
| | | 340200,芜湖市,3,340000 |
| | | 340300,蚌埠市,3,340000 |
| | | 340400,淮南市,3,340000 |
| | | 340500,马鞍山市,3,340000 |
| | | 340600,淮北市,3,340000 |
| | | 340700,铜陵市,3,340000 |
| | | 340800,安庆市,3,340000 |
| | | 341000,黄山市,3,340000 |
| | | 341100,滁州市,3,340000 |
| | | 341200,阜阳市,3,340000 |
| | | 341300,宿州市,3,340000 |
| | | 341500,六安市,3,340000 |
| | | 341600,亳州市,3,340000 |
| | | 341700,池州市,3,340000 |
| | | 341800,宣城市,3,340000 |
| | | 350100,福州市,3,350000 |
| | | 350200,厦门市,3,350000 |
| | | 350300,莆田市,3,350000 |
| | | 350400,三明市,3,350000 |
| | | 350500,泉州市,3,350000 |
| | | 350600,漳州市,3,350000 |
| | | 350700,南平市,3,350000 |
| | | 350800,龙岩市,3,350000 |
| | | 350900,宁德市,3,350000 |
| | | 360100,南昌市,3,360000 |
| | | 360200,景德镇市,3,360000 |
| | | 360300,萍乡市,3,360000 |
| | | 360400,九江市,3,360000 |
| | | 360500,新余市,3,360000 |
| | | 360600,鹰潭市,3,360000 |
| | | 360700,赣州市,3,360000 |
| | | 360800,吉安市,3,360000 |
| | | 360900,宜春市,3,360000 |
| | | 361000,抚州市,3,360000 |
| | | 361100,上饶市,3,360000 |
| | | 370100,济南市,3,370000 |
| | | 370200,青岛市,3,370000 |
| | | 370300,淄博市,3,370000 |
| | | 370400,枣庄市,3,370000 |
| | | 370500,东营市,3,370000 |
| | | 370600,烟台市,3,370000 |
| | | 370700,潍坊市,3,370000 |
| | | 370800,济宁市,3,370000 |
| | | 370900,泰安市,3,370000 |
| | | 371000,威海市,3,370000 |
| | | 371100,日照市,3,370000 |
| | | 371300,临沂市,3,370000 |
| | | 371400,德州市,3,370000 |
| | | 371500,聊城市,3,370000 |
| | | 371600,滨州市,3,370000 |
| | | 371700,菏泽市,3,370000 |
| | | 410100,郑州市,3,410000 |
| | | 410200,开封市,3,410000 |
| | | 410300,洛阳市,3,410000 |
| | | 410400,平顶山市,3,410000 |
| | | 410500,安阳市,3,410000 |
| | | 410600,鹤壁市,3,410000 |
| | | 410700,新乡市,3,410000 |
| | | 410800,焦作市,3,410000 |
| | | 410900,濮阳市,3,410000 |
| | | 411000,许昌市,3,410000 |
| | | 411100,漯河市,3,410000 |
| | | 411200,三门峡市,3,410000 |
| | | 411300,南阳市,3,410000 |
| | | 411400,商丘市,3,410000 |
| | | 411500,信阳市,3,410000 |
| | | 411600,周口市,3,410000 |
| | | 411700,驻马店市,3,410000 |
| | | 419000,省直辖县级行政区划,3,410000 |
| | | 420100,武汉市,3,420000 |
| | | 420200,黄石市,3,420000 |
| | | 420300,十堰市,3,420000 |
| | | 420500,宜昌市,3,420000 |
| | | 420600,襄阳市,3,420000 |
| | | 420700,鄂州市,3,420000 |
| | | 420800,荆门市,3,420000 |
| | | 420900,孝感市,3,420000 |
| | | 421000,荆州市,3,420000 |
| | | 421100,黄冈市,3,420000 |
| | | 421200,咸宁市,3,420000 |
| | | 421300,随州市,3,420000 |
| | | 422800,恩施土家族苗族自治州,3,420000 |
| | | 429000,省直辖县级行政区划,3,420000 |
| | | 430100,长沙市,3,430000 |
| | | 430200,株洲市,3,430000 |
| | | 430300,湘潭市,3,430000 |
| | | 430400,衡阳市,3,430000 |
| | | 430500,邵阳市,3,430000 |
| | | 430600,岳阳市,3,430000 |
| | | 430700,常德市,3,430000 |
| | | 430800,张家界市,3,430000 |
| | | 430900,益阳市,3,430000 |
| | | 431000,郴州市,3,430000 |
| | | 431100,永州市,3,430000 |
| | | 431200,怀化市,3,430000 |
| | | 431300,娄底市,3,430000 |
| | | 433100,湘西土家族苗族自治州,3,430000 |
| | | 440100,广州市,3,440000 |
| | | 440200,韶关市,3,440000 |
| | | 440300,深圳市,3,440000 |
| | | 440400,珠海市,3,440000 |
| | | 440500,汕头市,3,440000 |
| | | 440600,佛山市,3,440000 |
| | | 440700,江门市,3,440000 |
| | | 440800,湛江市,3,440000 |
| | | 440900,茂名市,3,440000 |
| | | 441200,肇庆市,3,440000 |
| | | 441300,惠州市,3,440000 |
| | | 441400,梅州市,3,440000 |
| | | 441500,汕尾市,3,440000 |
| | | 441600,河源市,3,440000 |
| | | 441700,阳江市,3,440000 |
| | | 441800,清远市,3,440000 |
| | | 441900,东莞市,3,440000 |
| | | 441901,莞城区,4,441900 |
| | | 441902,南城区,4,441900 |
| | | 441904,万江区,4,441900 |
| | | 441905,石碣镇,4,441900 |
| | | 441906,石龙镇,4,441900 |
| | | 441907,茶山镇,4,441900 |
| | | 441908,石排镇,4,441900 |
| | | 441909,企石镇,4,441900 |
| | | 441910,横沥镇,4,441900 |
| | | 441911,桥头镇,4,441900 |
| | | 441912,谢岗镇,4,441900 |
| | | 441913,东坑镇,4,441900 |
| | | 441914,常平镇,4,441900 |
| | | 441915,寮步镇,4,441900 |
| | | 441916,大朗镇,4,441900 |
| | | 441917,麻涌镇,4,441900 |
| | | 441918,中堂镇,4,441900 |
| | | 441919,高埗镇,4,441900 |
| | | 441920,樟木头镇,4,441900 |
| | | 441921,大岭山镇,4,441900 |
| | | 441922,望牛墩镇,4,441900 |
| | | 441923,黄江镇,4,441900 |
| | | 441924,洪梅镇,4,441900 |
| | | 441925,清溪镇,4,441900 |
| | | 441926,沙田镇,4,441900 |
| | | 441927,道滘镇,4,441900 |
| | | 441928,塘厦镇,4,441900 |
| | | 441929,虎门镇,4,441900 |
| | | 441930,厚街镇,4,441900 |
| | | 441931,凤岗镇,4,441900 |
| | | 441932,长安镇,4,441900 |
| | | 442000,中山市,3,440000 |
| | | 442001,石岐街道,4,442000 |
| | | 442002,东区街道,4,442000 |
| | | 442003,中山港街道,4,442000 |
| | | 442004,西区街道,4,442000 |
| | | 442005,南区街道,4,442000 |
| | | 442006,五桂山街道,4,442000 |
| | | 442007,民众街道,4,442000 |
| | | 442008,南朗街道,4,442000 |
| | | 442009,黄圃镇,4,442000 |
| | | 442010,东凤镇,4,442000 |
| | | 442011,古镇镇,4,442000 |
| | | 442012,沙溪镇,4,442000 |
| | | 442013,坦洲镇,4,442000 |
| | | 442014,港口镇,4,442000 |
| | | 442015,三角镇,4,442000 |
| | | 442016,横栏镇,4,442000 |
| | | 442017,南头镇,4,442000 |
| | | 442018,阜沙镇,4,442000 |
| | | 442019,三乡镇,4,442000 |
| | | 442020,板芙镇,4,442000 |
| | | 442021,大涌镇,4,442000 |
| | | 442022,神湾镇,4,442000 |
| | | 442023,小榄镇,4,442000 |
| | | 445100,潮州市,3,440000 |
| | | 445200,揭阳市,3,440000 |
| | | 445300,云浮市,3,440000 |
| | | 450100,南宁市,3,450000 |
| | | 450200,柳州市,3,450000 |
| | | 450300,桂林市,3,450000 |
| | | 450400,梧州市,3,450000 |
| | | 450500,北海市,3,450000 |
| | | 450600,防城港市,3,450000 |
| | | 450700,钦州市,3,450000 |
| | | 450800,贵港市,3,450000 |
| | | 450900,玉林市,3,450000 |
| | | 451000,百色市,3,450000 |
| | | 451100,贺州市,3,450000 |
| | | 451200,河池市,3,450000 |
| | | 451300,来宾市,3,450000 |
| | | 451400,崇左市,3,450000 |
| | | 460100,海口市,3,460000 |
| | | 460200,三亚市,3,460000 |
| | | 460300,三沙市,3,460000 |
| | | 460400,儋州市,3,460000 |
| | | 469000,省直辖县级行政区划,3,460000 |
| | | 500100,重庆市,3,500000 |
| | | 510100,成都市,3,510000 |
| | | 510300,自贡市,3,510000 |
| | | 510400,攀枝花市,3,510000 |
| | | 510500,泸州市,3,510000 |
| | | 510600,德阳市,3,510000 |
| | | 510700,绵阳市,3,510000 |
| | | 510800,广元市,3,510000 |
| | | 510900,遂宁市,3,510000 |
| | | 511000,内江市,3,510000 |
| | | 511100,乐山市,3,510000 |
| | | 511300,南充市,3,510000 |
| | | 511400,眉山市,3,510000 |
| | | 511500,宜宾市,3,510000 |
| | | 511600,广安市,3,510000 |
| | | 511700,达州市,3,510000 |
| | | 511800,雅安市,3,510000 |
| | | 511900,巴中市,3,510000 |
| | | 512000,资阳市,3,510000 |
| | | 513200,阿坝藏族羌族自治州,3,510000 |
| | | 513300,甘孜藏族自治州,3,510000 |
| | | 513400,凉山彝族自治州,3,510000 |
| | | 520100,贵阳市,3,520000 |
| | | 520200,六盘水市,3,520000 |
| | | 520300,遵义市,3,520000 |
| | | 520400,安顺市,3,520000 |
| | | 520500,毕节市,3,520000 |
| | | 520600,铜仁市,3,520000 |
| | | 522300,黔西南布依族苗族自治州,3,520000 |
| | | 522600,黔东南苗族侗族自治州,3,520000 |
| | | 522700,黔南布依族苗族自治州,3,520000 |
| | | 530100,昆明市,3,530000 |
| | | 530300,曲靖市,3,530000 |
| | | 530400,玉溪市,3,530000 |
| | | 530500,保山市,3,530000 |
| | | 530600,昭通市,3,530000 |
| | | 530700,丽江市,3,530000 |
| | | 530800,普洱市,3,530000 |
| | | 530900,临沧市,3,530000 |
| | | 532300,楚雄彝族自治州,3,530000 |
| | | 532500,红河哈尼族彝族自治州,3,530000 |
| | | 532600,文山壮族苗族自治州,3,530000 |
| | | 532800,西双版纳傣族自治州,3,530000 |
| | | 532900,大理白族自治州,3,530000 |
| | | 533100,德宏傣族景颇族自治州,3,530000 |
| | | 533300,怒江傈僳族自治州,3,530000 |
| | | 533400,迪庆藏族自治州,3,530000 |
| | | 540100,拉萨市,3,540000 |
| | | 540200,日喀则市,3,540000 |
| | | 540300,昌都市,3,540000 |
| | | 540400,林芝市,3,540000 |
| | | 540500,山南市,3,540000 |
| | | 540600,那曲市,3,540000 |
| | | 542500,阿里地区,3,540000 |
| | | 610100,西安市,3,610000 |
| | | 610200,铜川市,3,610000 |
| | | 610300,宝鸡市,3,610000 |
| | | 610400,咸阳市,3,610000 |
| | | 610500,渭南市,3,610000 |
| | | 610600,延安市,3,610000 |
| | | 610700,汉中市,3,610000 |
| | | 610800,榆林市,3,610000 |
| | | 610900,安康市,3,610000 |
| | | 611000,商洛市,3,610000 |
| | | 620100,兰州市,3,620000 |
| | | 620200,嘉峪关市,3,620000 |
| | | 620300,金昌市,3,620000 |
| | | 620400,白银市,3,620000 |
| | | 620500,天水市,3,620000 |
| | | 620600,武威市,3,620000 |
| | | 620700,张掖市,3,620000 |
| | | 620800,平凉市,3,620000 |
| | | 620900,酒泉市,3,620000 |
| | | 621000,庆阳市,3,620000 |
| | | 621100,定西市,3,620000 |
| | | 621200,陇南市,3,620000 |
| | | 622900,临夏回族自治州,3,620000 |
| | | 623000,甘南藏族自治州,3,620000 |
| | | 630100,西宁市,3,630000 |
| | | 630200,海东市,3,630000 |
| | | 632200,海北藏族自治州,3,630000 |
| | | 632300,黄南藏族自治州,3,630000 |
| | | 632500,海南藏族自治州,3,630000 |
| | | 632600,果洛藏族自治州,3,630000 |
| | | 632700,玉树藏族自治州,3,630000 |
| | | 632800,海西蒙古族藏族自治州,3,630000 |
| | | 640100,银川市,3,640000 |
| | | 640200,石嘴山市,3,640000 |
| | | 640300,吴忠市,3,640000 |
| | | 640400,固原市,3,640000 |
| | | 640500,中卫市,3,640000 |
| | | 650100,乌鲁木齐市,3,650000 |
| | | 650200,克拉玛依市,3,650000 |
| | | 650400,吐鲁番市,3,650000 |
| | | 650500,哈密市,3,650000 |
| | | 652300,昌吉回族自治州,3,650000 |
| | | 652700,博尔塔拉蒙古自治州,3,650000 |
| | | 652800,巴音郭楞蒙古自治州,3,650000 |
| | | 652900,阿克苏地区,3,650000 |
| | | 653000,克孜勒苏柯尔克孜自治州,3,650000 |
| | | 653100,喀什地区,3,650000 |
| | | 653200,和田地区,3,650000 |
| | | 654000,伊犁哈萨克自治州,3,650000 |
| | | 654200,塔城地区,3,650000 |
| | | 654300,阿勒泰地区,3,650000 |
| | | 659000,自治区直辖县级行政区划,3,650000 |
| | | 110101,东城区,4,110100 |
| | | 110102,西城区,4,110100 |
| | | 110105,朝阳区,4,110100 |
| | | 110106,丰台区,4,110100 |
| | | 110107,石景山区,4,110100 |
| | | 110108,海淀区,4,110100 |
| | | 110109,门头沟区,4,110100 |
| | | 110111,房山区,4,110100 |
| | | 110112,通州区,4,110100 |
| | | 110113,顺义区,4,110100 |
| | | 110114,昌平区,4,110100 |
| | | 110115,大兴区,4,110100 |
| | | 110116,怀柔区,4,110100 |
| | | 110117,平谷区,4,110100 |
| | | 110118,密云区,4,110100 |
| | | 110119,延庆区,4,110100 |
| | | 120101,和平区,4,120100 |
| | | 120102,河东区,4,120100 |
| | | 120103,河西区,4,120100 |
| | | 120104,南开区,4,120100 |
| | | 120105,河北区,4,120100 |
| | | 120106,红桥区,4,120100 |
| | | 120110,东丽区,4,120100 |
| | | 120111,西青区,4,120100 |
| | | 120112,津南区,4,120100 |
| | | 120113,北辰区,4,120100 |
| | | 120114,武清区,4,120100 |
| | | 120115,宝坻区,4,120100 |
| | | 120116,滨海新区,4,120100 |
| | | 120117,宁河区,4,120100 |
| | | 120118,静海区,4,120100 |
| | | 120119,蓟州区,4,120100 |
| | | 130102,长安区,4,130100 |
| | | 130104,桥西区,4,130100 |
| | | 130105,新华区,4,130100 |
| | | 130107,井陉矿区,4,130100 |
| | | 130108,裕华区,4,130100 |
| | | 130109,藁城区,4,130100 |
| | | 130110,鹿泉区,4,130100 |
| | | 130111,栾城区,4,130100 |
| | | 130121,井陉县,4,130100 |
| | | 130123,正定县,4,130100 |
| | | 130125,行唐县,4,130100 |
| | | 130126,灵寿县,4,130100 |
| | | 130127,高邑县,4,130100 |
| | | 130128,深泽县,4,130100 |
| | | 130129,赞皇县,4,130100 |
| | | 130130,无极县,4,130100 |
| | | 130131,平山县,4,130100 |
| | | 130132,元氏县,4,130100 |
| | | 130133,赵县,4,130100 |
| | | 130171,石家庄高新技术产业开发区,4,130100 |
| | | 130172,石家庄循环化工园区,4,130100 |
| | | 130181,辛集市,4,130100 |
| | | 130183,晋州市,4,130100 |
| | | 130184,新乐市,4,130100 |
| | | 130202,路南区,4,130200 |
| | | 130203,路北区,4,130200 |
| | | 130204,古冶区,4,130200 |
| | | 130205,开平区,4,130200 |
| | | 130207,丰南区,4,130200 |
| | | 130208,丰润区,4,130200 |
| | | 130209,曹妃甸区,4,130200 |
| | | 130224,滦南县,4,130200 |
| | | 130225,乐亭县,4,130200 |
| | | 130227,迁西县,4,130200 |
| | | 130229,玉田县,4,130200 |
| | | 130271,河北唐山芦台经济开发区,4,130200 |
| | | 130272,唐山市汉沽管理区,4,130200 |
| | | 130273,唐山高新技术产业开发区,4,130200 |
| | | 130274,河北唐山海港经济开发区,4,130200 |
| | | 130281,遵化市,4,130200 |
| | | 130283,迁安市,4,130200 |
| | | 130284,滦州市,4,130200 |
| | | 130302,海港区,4,130300 |
| | | 130303,山海关区,4,130300 |
| | | 130304,北戴河区,4,130300 |
| | | 130306,抚宁区,4,130300 |
| | | 130321,青龙满族自治县,4,130300 |
| | | 130322,昌黎县,4,130300 |
| | | 130324,卢龙县,4,130300 |
| | | 130371,秦皇岛市经济技术开发区,4,130300 |
| | | 130372,北戴河新区,4,130300 |
| | | 130402,邯山区,4,130400 |
| | | 130403,丛台区,4,130400 |
| | | 130404,复兴区,4,130400 |
| | | 130406,峰峰矿区,4,130400 |
| | | 130407,肥乡区,4,130400 |
| | | 130408,永年区,4,130400 |
| | | 130423,临漳县,4,130400 |
| | | 130424,成安县,4,130400 |
| | | 130425,大名县,4,130400 |
| | | 130426,涉县,4,130400 |
| | | 130427,磁县,4,130400 |
| | | 130430,邱县,4,130400 |
| | | 130431,鸡泽县,4,130400 |
| | | 130432,广平县,4,130400 |
| | | 130433,馆陶县,4,130400 |
| | | 130434,魏县,4,130400 |
| | | 130435,曲周县,4,130400 |
| | | 130471,邯郸经济技术开发区,4,130400 |
| | | 130473,邯郸冀南新区,4,130400 |
| | | 130481,武安市,4,130400 |
| | | 130502,襄都区,4,130500 |
| | | 130503,信都区,4,130500 |
| | | 130505,任泽区,4,130500 |
| | | 130506,南和区,4,130500 |
| | | 130522,临城县,4,130500 |
| | | 130523,内丘县,4,130500 |
| | | 130524,柏乡县,4,130500 |
| | | 130525,隆尧县,4,130500 |
| | | 130528,宁晋县,4,130500 |
| | | 130529,巨鹿县,4,130500 |
| | | 130530,新河县,4,130500 |
| | | 130531,广宗县,4,130500 |
| | | 130532,平乡县,4,130500 |
| | | 130533,威县,4,130500 |
| | | 130534,清河县,4,130500 |
| | | 130535,临西县,4,130500 |
| | | 130571,河北邢台经济开发区,4,130500 |
| | | 130581,南宫市,4,130500 |
| | | 130582,沙河市,4,130500 |
| | | 130602,竞秀区,4,130600 |
| | | 130606,莲池区,4,130600 |
| | | 130607,满城区,4,130600 |
| | | 130608,清苑区,4,130600 |
| | | 130609,徐水区,4,130600 |
| | | 130623,涞水县,4,130600 |
| | | 130624,阜平县,4,130600 |
| | | 130626,定兴县,4,130600 |
| | | 130627,唐县,4,130600 |
| | | 130628,高阳县,4,130600 |
| | | 130629,容城县,4,130600 |
| | | 130630,涞源县,4,130600 |
| | | 130631,望都县,4,130600 |
| | | 130632,安新县,4,130600 |
| | | 130633,易县,4,130600 |
| | | 130634,曲阳县,4,130600 |
| | | 130635,蠡县,4,130600 |
| | | 130636,顺平县,4,130600 |
| | | 130637,博野县,4,130600 |
| | | 130638,雄县,4,130600 |
| | | 130671,保定高新技术产业开发区,4,130600 |
| | | 130672,保定白沟新城,4,130600 |
| | | 130681,涿州市,4,130600 |
| | | 130682,定州市,4,130600 |
| | | 130683,安国市,4,130600 |
| | | 130684,高碑店市,4,130600 |
| | | 130702,桥东区,4,130700 |
| | | 130703,桥西区,4,130700 |
| | | 130705,宣化区,4,130700 |
| | | 130706,下花园区,4,130700 |
| | | 130708,万全区,4,130700 |
| | | 130709,崇礼区,4,130700 |
| | | 130722,张北县,4,130700 |
| | | 130723,康保县,4,130700 |
| | | 130724,沽源县,4,130700 |
| | | 130725,尚义县,4,130700 |
| | | 130726,蔚县,4,130700 |
| | | 130727,阳原县,4,130700 |
| | | 130728,怀安县,4,130700 |
| | | 130730,怀来县,4,130700 |
| | | 130731,涿鹿县,4,130700 |
| | | 130732,赤城县,4,130700 |
| | | 130771,张家口经济开发区,4,130700 |
| | | 130772,张家口市察北管理区,4,130700 |
| | | 130773,张家口市塞北管理区,4,130700 |
| | | 130802,双桥区,4,130800 |
| | | 130803,双滦区,4,130800 |
| | | 130804,鹰手营子矿区,4,130800 |
| | | 130821,承德县,4,130800 |
| | | 130822,兴隆县,4,130800 |
| | | 130824,滦平县,4,130800 |
| | | 130825,隆化县,4,130800 |
| | | 130826,丰宁满族自治县,4,130800 |
| | | 130827,宽城满族自治县,4,130800 |
| | | 130828,围场满族蒙古族自治县,4,130800 |
| | | 130871,承德高新技术产业开发区,4,130800 |
| | | 130881,平泉市,4,130800 |
| | | 130902,新华区,4,130900 |
| | | 130903,运河区,4,130900 |
| | | 130921,沧县,4,130900 |
| | | 130922,青县,4,130900 |
| | | 130923,东光县,4,130900 |
| | | 130924,海兴县,4,130900 |
| | | 130925,盐山县,4,130900 |
| | | 130926,肃宁县,4,130900 |
| | | 130927,南皮县,4,130900 |
| | | 130928,吴桥县,4,130900 |
| | | 130929,献县,4,130900 |
| | | 130930,孟村回族自治县,4,130900 |
| | | 130971,河北沧州经济开发区,4,130900 |
| | | 130972,沧州高新技术产业开发区,4,130900 |
| | | 130973,沧州渤海新区,4,130900 |
| | | 130981,泊头市,4,130900 |
| | | 130982,任丘市,4,130900 |
| | | 130983,黄骅市,4,130900 |
| | | 130984,河间市,4,130900 |
| | | 131002,安次区,4,131000 |
| | | 131003,广阳区,4,131000 |
| | | 131022,固安县,4,131000 |
| | | 131023,永清县,4,131000 |
| | | 131024,香河县,4,131000 |
| | | 131025,大城县,4,131000 |
| | | 131026,文安县,4,131000 |
| | | 131028,大厂回族自治县,4,131000 |
| | | 131071,廊坊经济技术开发区,4,131000 |
| | | 131081,霸州市,4,131000 |
| | | 131082,三河市,4,131000 |
| | | 131102,桃城区,4,131100 |
| | | 131103,冀州区,4,131100 |
| | | 131121,枣强县,4,131100 |
| | | 131122,武邑县,4,131100 |
| | | 131123,武强县,4,131100 |
| | | 131124,饶阳县,4,131100 |
| | | 131125,安平县,4,131100 |
| | | 131126,故城县,4,131100 |
| | | 131127,景县,4,131100 |
| | | 131128,阜城县,4,131100 |
| | | 131171,河北衡水高新技术产业开发区,4,131100 |
| | | 131172,衡水滨湖新区,4,131100 |
| | | 131182,深州市,4,131100 |
| | | 140105,小店区,4,140100 |
| | | 140106,迎泽区,4,140100 |
| | | 140107,杏花岭区,4,140100 |
| | | 140108,尖草坪区,4,140100 |
| | | 140109,万柏林区,4,140100 |
| | | 140110,晋源区,4,140100 |
| | | 140121,清徐县,4,140100 |
| | | 140122,阳曲县,4,140100 |
| | | 140123,娄烦县,4,140100 |
| | | 140171,山西转型综合改革示范区,4,140100 |
| | | 140181,古交市,4,140100 |
| | | 140212,新荣区,4,140200 |
| | | 140213,平城区,4,140200 |
| | | 140214,云冈区,4,140200 |
| | | 140215,云州区,4,140200 |
| | | 140221,阳高县,4,140200 |
| | | 140222,天镇县,4,140200 |
| | | 140223,广灵县,4,140200 |
| | | 140224,灵丘县,4,140200 |
| | | 140225,浑源县,4,140200 |
| | | 140226,左云县,4,140200 |
| | | 140271,山西大同经济开发区,4,140200 |
| | | 140302,城区,4,140300 |
| | | 140303,矿区,4,140300 |
| | | 140311,郊区,4,140300 |
| | | 140321,平定县,4,140300 |
| | | 140322,盂县,4,140300 |
| | | 140403,潞州区,4,140400 |
| | | 140404,上党区,4,140400 |
| | | 140405,屯留区,4,140400 |
| | | 140406,潞城区,4,140400 |
| | | 140423,襄垣县,4,140400 |
| | | 140425,平顺县,4,140400 |
| | | 140426,黎城县,4,140400 |
| | | 140427,壶关县,4,140400 |
| | | 140428,长子县,4,140400 |
| | | 140429,武乡县,4,140400 |
| | | 140430,沁县,4,140400 |
| | | 140431,沁源县,4,140400 |
| | | 140471,山西长治高新技术产业园区,4,140400 |
| | | 140502,城区,4,140500 |
| | | 140521,沁水县,4,140500 |
| | | 140522,阳城县,4,140500 |
| | | 140524,陵川县,4,140500 |
| | | 140525,泽州县,4,140500 |
| | | 140581,高平市,4,140500 |
| | | 140602,朔城区,4,140600 |
| | | 140603,平鲁区,4,140600 |
| | | 140621,山阴县,4,140600 |
| | | 140622,应县,4,140600 |
| | | 140623,右玉县,4,140600 |
| | | 140671,山西朔州经济开发区,4,140600 |
| | | 140681,怀仁市,4,140600 |
| | | 140702,榆次区,4,140700 |
| | | 140703,太谷区,4,140700 |
| | | 140721,榆社县,4,140700 |
| | | 140722,左权县,4,140700 |
| | | 140723,和顺县,4,140700 |
| | | 140724,昔阳县,4,140700 |
| | | 140725,寿阳县,4,140700 |
| | | 140727,祁县,4,140700 |
| | | 140728,平遥县,4,140700 |
| | | 140729,灵石县,4,140700 |
| | | 140781,介休市,4,140700 |
| | | 140802,盐湖区,4,140800 |
| | | 140821,临猗县,4,140800 |
| | | 140822,万荣县,4,140800 |
| | | 140823,闻喜县,4,140800 |
| | | 140824,稷山县,4,140800 |
| | | 140825,新绛县,4,140800 |
| | | 140826,绛县,4,140800 |
| | | 140827,垣曲县,4,140800 |
| | | 140828,夏县,4,140800 |
| | | 140829,平陆县,4,140800 |
| | | 140830,芮城县,4,140800 |
| | | 140881,永济市,4,140800 |
| | | 140882,河津市,4,140800 |
| | | 140902,忻府区,4,140900 |
| | | 140921,定襄县,4,140900 |
| | | 140922,五台县,4,140900 |
| | | 140923,代县,4,140900 |
| | | 140924,繁峙县,4,140900 |
| | | 140925,宁武县,4,140900 |
| | | 140926,静乐县,4,140900 |
| | | 140927,神池县,4,140900 |
| | | 140928,五寨县,4,140900 |
| | | 140929,岢岚县,4,140900 |
| | | 140930,河曲县,4,140900 |
| | | 140931,保德县,4,140900 |
| | | 140932,偏关县,4,140900 |
| | | 140971,五台山风景名胜区,4,140900 |
| | | 140981,原平市,4,140900 |
| | | 141002,尧都区,4,141000 |
| | | 141021,曲沃县,4,141000 |
| | | 141022,翼城县,4,141000 |
| | | 141023,襄汾县,4,141000 |
| | | 141024,洪洞县,4,141000 |
| | | 141025,古县,4,141000 |
| | | 141026,安泽县,4,141000 |
| | | 141027,浮山县,4,141000 |
| | | 141028,吉县,4,141000 |
| | | 141029,乡宁县,4,141000 |
| | | 141030,大宁县,4,141000 |
| | | 141031,隰县,4,141000 |
| | | 141032,永和县,4,141000 |
| | | 141033,蒲县,4,141000 |
| | | 141034,汾西县,4,141000 |
| | | 141081,侯马市,4,141000 |
| | | 141082,霍州市,4,141000 |
| | | 141102,离石区,4,141100 |
| | | 141121,文水县,4,141100 |
| | | 141122,交城县,4,141100 |
| | | 141123,兴县,4,141100 |
| | | 141124,临县,4,141100 |
| | | 141125,柳林县,4,141100 |
| | | 141126,石楼县,4,141100 |
| | | 141127,岚县,4,141100 |
| | | 141128,方山县,4,141100 |
| | | 141129,中阳县,4,141100 |
| | | 141130,交口县,4,141100 |
| | | 141181,孝义市,4,141100 |
| | | 141182,汾阳市,4,141100 |
| | | 150102,新城区,4,150100 |
| | | 150103,回民区,4,150100 |
| | | 150104,玉泉区,4,150100 |
| | | 150105,赛罕区,4,150100 |
| | | 150121,土默特左旗,4,150100 |
| | | 150122,托克托县,4,150100 |
| | | 150123,和林格尔县,4,150100 |
| | | 150124,清水河县,4,150100 |
| | | 150125,武川县,4,150100 |
| | | 150172,呼和浩特经济技术开发区,4,150100 |
| | | 150202,东河区,4,150200 |
| | | 150203,昆都仑区,4,150200 |
| | | 150204,青山区,4,150200 |
| | | 150205,石拐区,4,150200 |
| | | 150206,白云鄂博矿区,4,150200 |
| | | 150207,九原区,4,150200 |
| | | 150221,土默特右旗,4,150200 |
| | | 150222,固阳县,4,150200 |
| | | 150223,达尔罕茂明安联合旗,4,150200 |
| | | 150271,包头稀土高新技术产业开发区,4,150200 |
| | | 150302,海勃湾区,4,150300 |
| | | 150303,海南区,4,150300 |
| | | 150304,乌达区,4,150300 |
| | | 150402,红山区,4,150400 |
| | | 150403,元宝山区,4,150400 |
| | | 150404,松山区,4,150400 |
| | | 150421,阿鲁科尔沁旗,4,150400 |
| | | 150422,巴林左旗,4,150400 |
| | | 150423,巴林右旗,4,150400 |
| | | 150424,林西县,4,150400 |
| | | 150425,克什克腾旗,4,150400 |
| | | 150426,翁牛特旗,4,150400 |
| | | 150428,喀喇沁旗,4,150400 |
| | | 150429,宁城县,4,150400 |
| | | 150430,敖汉旗,4,150400 |
| | | 150502,科尔沁区,4,150500 |
| | | 150521,科尔沁左翼中旗,4,150500 |
| | | 150522,科尔沁左翼后旗,4,150500 |
| | | 150523,开鲁县,4,150500 |
| | | 150524,库伦旗,4,150500 |
| | | 150525,奈曼旗,4,150500 |
| | | 150526,扎鲁特旗,4,150500 |
| | | 150571,通辽经济技术开发区,4,150500 |
| | | 150581,霍林郭勒市,4,150500 |
| | | 150602,东胜区,4,150600 |
| | | 150603,康巴什区,4,150600 |
| | | 150621,达拉特旗,4,150600 |
| | | 150622,准格尔旗,4,150600 |
| | | 150623,鄂托克前旗,4,150600 |
| | | 150624,鄂托克旗,4,150600 |
| | | 150625,杭锦旗,4,150600 |
| | | 150626,乌审旗,4,150600 |
| | | 150627,伊金霍洛旗,4,150600 |
| | | 150702,海拉尔区,4,150700 |
| | | 150703,扎赉诺尔区,4,150700 |
| | | 150721,阿荣旗,4,150700 |
| | | 150722,莫力达瓦达斡尔族自治旗,4,150700 |
| | | 150723,鄂伦春自治旗,4,150700 |
| | | 150724,鄂温克族自治旗,4,150700 |
| | | 150725,陈巴尔虎旗,4,150700 |
| | | 150726,新巴尔虎左旗,4,150700 |
| | | 150727,新巴尔虎右旗,4,150700 |
| | | 150781,满洲里市,4,150700 |
| | | 150782,牙克石市,4,150700 |
| | | 150783,扎兰屯市,4,150700 |
| | | 150784,额尔古纳市,4,150700 |
| | | 150785,根河市,4,150700 |
| | | 150802,临河区,4,150800 |
| | | 150821,五原县,4,150800 |
| | | 150822,磴口县,4,150800 |
| | | 150823,乌拉特前旗,4,150800 |
| | | 150824,乌拉特中旗,4,150800 |
| | | 150825,乌拉特后旗,4,150800 |
| | | 150826,杭锦后旗,4,150800 |
| | | 150902,集宁区,4,150900 |
| | | 150921,卓资县,4,150900 |
| | | 150922,化德县,4,150900 |
| | | 150923,商都县,4,150900 |
| | | 150924,兴和县,4,150900 |
| | | 150925,凉城县,4,150900 |
| | | 150926,察哈尔右翼前旗,4,150900 |
| | | 150927,察哈尔右翼中旗,4,150900 |
| | | 150928,察哈尔右翼后旗,4,150900 |
| | | 150929,四子王旗,4,150900 |
| | | 150981,丰镇市,4,150900 |
| | | 152201,乌兰浩特市,4,152200 |
| | | 152202,阿尔山市,4,152200 |
| | | 152221,科尔沁右翼前旗,4,152200 |
| | | 152222,科尔沁右翼中旗,4,152200 |
| | | 152223,扎赉特旗,4,152200 |
| | | 152224,突泉县,4,152200 |
| | | 152501,二连浩特市,4,152500 |
| | | 152502,锡林浩特市,4,152500 |
| | | 152522,阿巴嘎旗,4,152500 |
| | | 152523,苏尼特左旗,4,152500 |
| | | 152524,苏尼特右旗,4,152500 |
| | | 152525,东乌珠穆沁旗,4,152500 |
| | | 152526,西乌珠穆沁旗,4,152500 |
| | | 152527,太仆寺旗,4,152500 |
| | | 152528,镶黄旗,4,152500 |
| | | 152529,正镶白旗,4,152500 |
| | | 152530,正蓝旗,4,152500 |
| | | 152531,多伦县,4,152500 |
| | | 152571,乌拉盖管委会,4,152500 |
| | | 152921,阿拉善左旗,4,152900 |
| | | 152922,阿拉善右旗,4,152900 |
| | | 152923,额济纳旗,4,152900 |
| | | 152971,内蒙古阿拉善高新技术产业开发区,4,152900 |
| | | 210102,和平区,4,210100 |
| | | 210103,沈河区,4,210100 |
| | | 210104,大东区,4,210100 |
| | | 210105,皇姑区,4,210100 |
| | | 210106,铁西区,4,210100 |
| | | 210111,苏家屯区,4,210100 |
| | | 210112,浑南区,4,210100 |
| | | 210113,沈北新区,4,210100 |
| | | 210114,于洪区,4,210100 |
| | | 210115,辽中区,4,210100 |
| | | 210123,康平县,4,210100 |
| | | 210124,法库县,4,210100 |
| | | 210181,新民市,4,210100 |
| | | 210202,中山区,4,210200 |
| | | 210203,西岗区,4,210200 |
| | | 210204,沙河口区,4,210200 |
| | | 210211,甘井子区,4,210200 |
| | | 210212,旅顺口区,4,210200 |
| | | 210213,金州区,4,210200 |
| | | 210214,普兰店区,4,210200 |
| | | 210224,长海县,4,210200 |
| | | 210281,瓦房店市,4,210200 |
| | | 210283,庄河市,4,210200 |
| | | 210302,铁东区,4,210300 |
| | | 210303,铁西区,4,210300 |
| | | 210304,立山区,4,210300 |
| | | 210311,千山区,4,210300 |
| | | 210321,台安县,4,210300 |
| | | 210323,岫岩满族自治县,4,210300 |
| | | 210381,海城市,4,210300 |
| | | 210402,新抚区,4,210400 |
| | | 210403,东洲区,4,210400 |
| | | 210404,望花区,4,210400 |
| | | 210411,顺城区,4,210400 |
| | | 210421,抚顺县,4,210400 |
| | | 210422,新宾满族自治县,4,210400 |
| | | 210423,清原满族自治县,4,210400 |
| | | 210502,平山区,4,210500 |
| | | 210503,溪湖区,4,210500 |
| | | 210504,明山区,4,210500 |
| | | 210505,南芬区,4,210500 |
| | | 210521,本溪满族自治县,4,210500 |
| | | 210522,桓仁满族自治县,4,210500 |
| | | 210602,元宝区,4,210600 |
| | | 210603,振兴区,4,210600 |
| | | 210604,振安区,4,210600 |
| | | 210624,宽甸满族自治县,4,210600 |
| | | 210681,东港市,4,210600 |
| | | 210682,凤城市,4,210600 |
| | | 210702,古塔区,4,210700 |
| | | 210703,凌河区,4,210700 |
| | | 210711,太和区,4,210700 |
| | | 210726,黑山县,4,210700 |
| | | 210727,义县,4,210700 |
| | | 210781,凌海市,4,210700 |
| | | 210782,北镇市,4,210700 |
| | | 210802,站前区,4,210800 |
| | | 210803,西市区,4,210800 |
| | | 210804,鲅鱼圈区,4,210800 |
| | | 210811,老边区,4,210800 |
| | | 210881,盖州市,4,210800 |
| | | 210882,大石桥市,4,210800 |
| | | 210902,海州区,4,210900 |
| | | 210903,新邱区,4,210900 |
| | | 210904,太平区,4,210900 |
| | | 210905,清河门区,4,210900 |
| | | 210911,细河区,4,210900 |
| | | 210921,阜新蒙古族自治县,4,210900 |
| | | 210922,彰武县,4,210900 |
| | | 211002,白塔区,4,211000 |
| | | 211003,文圣区,4,211000 |
| | | 211004,宏伟区,4,211000 |
| | | 211005,弓长岭区,4,211000 |
| | | 211011,太子河区,4,211000 |
| | | 211021,辽阳县,4,211000 |
| | | 211081,灯塔市,4,211000 |
| | | 211102,双台子区,4,211100 |
| | | 211103,兴隆台区,4,211100 |
| | | 211104,大洼区,4,211100 |
| | | 211122,盘山县,4,211100 |
| | | 211202,银州区,4,211200 |
| | | 211204,清河区,4,211200 |
| | | 211221,铁岭县,4,211200 |
| | | 211223,西丰县,4,211200 |
| | | 211224,昌图县,4,211200 |
| | | 211281,调兵山市,4,211200 |
| | | 211282,开原市,4,211200 |
| | | 211302,双塔区,4,211300 |
| | | 211303,龙城区,4,211300 |
| | | 211321,朝阳县,4,211300 |
| | | 211322,建平县,4,211300 |
| | | 211324,喀喇沁左翼蒙古族自治县,4,211300 |
| | | 211381,北票市,4,211300 |
| | | 211382,凌源市,4,211300 |
| | | 211402,连山区,4,211400 |
| | | 211403,龙港区,4,211400 |
| | | 211404,南票区,4,211400 |
| | | 211421,绥中县,4,211400 |
| | | 211422,建昌县,4,211400 |
| | | 211481,兴城市,4,211400 |
| | | 220102,南关区,4,220100 |
| | | 220103,宽城区,4,220100 |
| | | 220104,朝阳区,4,220100 |
| | | 220105,二道区,4,220100 |
| | | 220106,绿园区,4,220100 |
| | | 220112,双阳区,4,220100 |
| | | 220113,九台区,4,220100 |
| | | 220122,农安县,4,220100 |
| | | 220171,长春经济技术开发区,4,220100 |
| | | 220172,长春净月高新技术产业开发区,4,220100 |
| | | 220173,长春高新技术产业开发区,4,220100 |
| | | 220174,长春汽车经济技术开发区,4,220100 |
| | | 220182,榆树市,4,220100 |
| | | 220183,德惠市,4,220100 |
| | | 220184,公主岭市,4,220100 |
| | | 220202,昌邑区,4,220200 |
| | | 220203,龙潭区,4,220200 |
| | | 220204,船营区,4,220200 |
| | | 220211,丰满区,4,220200 |
| | | 220221,永吉县,4,220200 |
| | | 220271,吉林经济开发区,4,220200 |
| | | 220272,吉林高新技术产业开发区,4,220200 |
| | | 220273,吉林中国新加坡食品区,4,220200 |
| | | 220281,蛟河市,4,220200 |
| | | 220282,桦甸市,4,220200 |
| | | 220283,舒兰市,4,220200 |
| | | 220284,磐石市,4,220200 |
| | | 220302,铁西区,4,220300 |
| | | 220303,铁东区,4,220300 |
| | | 220322,梨树县,4,220300 |
| | | 220323,伊通满族自治县,4,220300 |
| | | 220382,双辽市,4,220300 |
| | | 220402,龙山区,4,220400 |
| | | 220403,西安区,4,220400 |
| | | 220421,东丰县,4,220400 |
| | | 220422,东辽县,4,220400 |
| | | 220502,东昌区,4,220500 |
| | | 220503,二道江区,4,220500 |
| | | 220521,通化县,4,220500 |
| | | 220523,辉南县,4,220500 |
| | | 220524,柳河县,4,220500 |
| | | 220581,梅河口市,4,220500 |
| | | 220582,集安市,4,220500 |
| | | 220602,浑江区,4,220600 |
| | | 220605,江源区,4,220600 |
| | | 220621,抚松县,4,220600 |
| | | 220622,靖宇县,4,220600 |
| | | 220623,长白朝鲜族自治县,4,220600 |
| | | 220681,临江市,4,220600 |
| | | 220702,宁江区,4,220700 |
| | | 220721,前郭尔罗斯蒙古族自治县,4,220700 |
| | | 220722,长岭县,4,220700 |
| | | 220723,乾安县,4,220700 |
| | | 220771,吉林松原经济开发区,4,220700 |
| | | 220781,扶余市,4,220700 |
| | | 220802,洮北区,4,220800 |
| | | 220821,镇赉县,4,220800 |
| | | 220822,通榆县,4,220800 |
| | | 220871,吉林白城经济开发区,4,220800 |
| | | 220881,洮南市,4,220800 |
| | | 220882,大安市,4,220800 |
| | | 222401,延吉市,4,222400 |
| | | 222402,图们市,4,222400 |
| | | 222403,敦化市,4,222400 |
| | | 222404,珲春市,4,222400 |
| | | 222405,龙井市,4,222400 |
| | | 222406,和龙市,4,222400 |
| | | 222424,汪清县,4,222400 |
| | | 222426,安图县,4,222400 |
| | | 230102,道里区,4,230100 |
| | | 230103,南岗区,4,230100 |
| | | 230104,道外区,4,230100 |
| | | 230108,平房区,4,230100 |
| | | 230109,松北区,4,230100 |
| | | 230110,香坊区,4,230100 |
| | | 230111,呼兰区,4,230100 |
| | | 230112,阿城区,4,230100 |
| | | 230113,双城区,4,230100 |
| | | 230123,依兰县,4,230100 |
| | | 230124,方正县,4,230100 |
| | | 230125,宾县,4,230100 |
| | | 230126,巴彦县,4,230100 |
| | | 230127,木兰县,4,230100 |
| | | 230128,通河县,4,230100 |
| | | 230129,延寿县,4,230100 |
| | | 230183,尚志市,4,230100 |
| | | 230184,五常市,4,230100 |
| | | 230202,龙沙区,4,230200 |
| | | 230203,建华区,4,230200 |
| | | 230204,铁锋区,4,230200 |
| | | 230205,昂昂溪区,4,230200 |
| | | 230206,富拉尔基区,4,230200 |
| | | 230207,碾子山区,4,230200 |
| | | 230208,梅里斯达斡尔族区,4,230200 |
| | | 230221,龙江县,4,230200 |
| | | 230223,依安县,4,230200 |
| | | 230224,泰来县,4,230200 |
| | | 230225,甘南县,4,230200 |
| | | 230227,富裕县,4,230200 |
| | | 230229,克山县,4,230200 |
| | | 230230,克东县,4,230200 |
| | | 230231,拜泉县,4,230200 |
| | | 230281,讷河市,4,230200 |
| | | 230302,鸡冠区,4,230300 |
| | | 230303,恒山区,4,230300 |
| | | 230304,滴道区,4,230300 |
| | | 230305,梨树区,4,230300 |
| | | 230306,城子河区,4,230300 |
| | | 230307,麻山区,4,230300 |
| | | 230321,鸡东县,4,230300 |
| | | 230381,虎林市,4,230300 |
| | | 230382,密山市,4,230300 |
| | | 230402,向阳区,4,230400 |
| | | 230403,工农区,4,230400 |
| | | 230404,南山区,4,230400 |
| | | 230405,兴安区,4,230400 |
| | | 230406,东山区,4,230400 |
| | | 230407,兴山区,4,230400 |
| | | 230421,萝北县,4,230400 |
| | | 230422,绥滨县,4,230400 |
| | | 230502,尖山区,4,230500 |
| | | 230503,岭东区,4,230500 |
| | | 230505,四方台区,4,230500 |
| | | 230506,宝山区,4,230500 |
| | | 230521,集贤县,4,230500 |
| | | 230522,友谊县,4,230500 |
| | | 230523,宝清县,4,230500 |
| | | 230524,饶河县,4,230500 |
| | | 230602,萨尔图区,4,230600 |
| | | 230603,龙凤区,4,230600 |
| | | 230604,让胡路区,4,230600 |
| | | 230605,红岗区,4,230600 |
| | | 230606,大同区,4,230600 |
| | | 230621,肇州县,4,230600 |
| | | 230622,肇源县,4,230600 |
| | | 230623,林甸县,4,230600 |
| | | 230624,杜尔伯特蒙古族自治县,4,230600 |
| | | 230671,大庆高新技术产业开发区,4,230600 |
| | | 230717,伊美区,4,230700 |
| | | 230718,乌翠区,4,230700 |
| | | 230719,友好区,4,230700 |
| | | 230722,嘉荫县,4,230700 |
| | | 230723,汤旺县,4,230700 |
| | | 230724,丰林县,4,230700 |
| | | 230725,大箐山县,4,230700 |
| | | 230726,南岔县,4,230700 |
| | | 230751,金林区,4,230700 |
| | | 230781,铁力市,4,230700 |
| | | 230803,向阳区,4,230800 |
| | | 230804,前进区,4,230800 |
| | | 230805,东风区,4,230800 |
| | | 230811,郊区,4,230800 |
| | | 230822,桦南县,4,230800 |
| | | 230826,桦川县,4,230800 |
| | | 230828,汤原县,4,230800 |
| | | 230881,同江市,4,230800 |
| | | 230882,富锦市,4,230800 |
| | | 230883,抚远市,4,230800 |
| | | 230902,新兴区,4,230900 |
| | | 230903,桃山区,4,230900 |
| | | 230904,茄子河区,4,230900 |
| | | 230921,勃利县,4,230900 |
| | | 231002,东安区,4,231000 |
| | | 231003,阳明区,4,231000 |
| | | 231004,爱民区,4,231000 |
| | | 231005,西安区,4,231000 |
| | | 231025,林口县,4,231000 |
| | | 231071,牡丹江经济技术开发区,4,231000 |
| | | 231081,绥芬河市,4,231000 |
| | | 231083,海林市,4,231000 |
| | | 231084,宁安市,4,231000 |
| | | 231085,穆棱市,4,231000 |
| | | 231086,东宁市,4,231000 |
| | | 231102,爱辉区,4,231100 |
| | | 231123,逊克县,4,231100 |
| | | 231124,孙吴县,4,231100 |
| | | 231181,北安市,4,231100 |
| | | 231182,五大连池市,4,231100 |
| | | 231183,嫩江市,4,231100 |
| | | 231202,北林区,4,231200 |
| | | 231221,望奎县,4,231200 |
| | | 231222,兰西县,4,231200 |
| | | 231223,青冈县,4,231200 |
| | | 231224,庆安县,4,231200 |
| | | 231225,明水县,4,231200 |
| | | 231226,绥棱县,4,231200 |
| | | 231281,安达市,4,231200 |
| | | 231282,肇东市,4,231200 |
| | | 231283,海伦市,4,231200 |
| | | 232701,漠河市,4,232700 |
| | | 232721,呼玛县,4,232700 |
| | | 232722,塔河县,4,232700 |
| | | 232761,加格达奇区,4,232700 |
| | | 232762,松岭区,4,232700 |
| | | 232763,新林区,4,232700 |
| | | 232764,呼中区,4,232700 |
| | | 310101,黄浦区,4,310100 |
| | | 310104,徐汇区,4,310100 |
| | | 310105,长宁区,4,310100 |
| | | 310106,静安区,4,310100 |
| | | 310107,普陀区,4,310100 |
| | | 310109,虹口区,4,310100 |
| | | 310110,杨浦区,4,310100 |
| | | 310112,闵行区,4,310100 |
| | | 310113,宝山区,4,310100 |
| | | 310114,嘉定区,4,310100 |
| | | 310115,浦东新区,4,310100 |
| | | 310116,金山区,4,310100 |
| | | 310117,松江区,4,310100 |
| | | 310118,青浦区,4,310100 |
| | | 310120,奉贤区,4,310100 |
| | | 310151,崇明区,4,310100 |
| | | 320102,玄武区,4,320100 |
| | | 320104,秦淮区,4,320100 |
| | | 320105,建邺区,4,320100 |
| | | 320106,鼓楼区,4,320100 |
| | | 320111,浦口区,4,320100 |
| | | 320113,栖霞区,4,320100 |
| | | 320114,雨花台区,4,320100 |
| | | 320115,江宁区,4,320100 |
| | | 320116,六合区,4,320100 |
| | | 320117,溧水区,4,320100 |
| | | 320118,高淳区,4,320100 |
| | | 320205,锡山区,4,320200 |
| | | 320206,惠山区,4,320200 |
| | | 320211,滨湖区,4,320200 |
| | | 320213,梁溪区,4,320200 |
| | | 320214,新吴区,4,320200 |
| | | 320281,江阴市,4,320200 |
| | | 320282,宜兴市,4,320200 |
| | | 320302,鼓楼区,4,320300 |
| | | 320303,云龙区,4,320300 |
| | | 320305,贾汪区,4,320300 |
| | | 320311,泉山区,4,320300 |
| | | 320312,铜山区,4,320300 |
| | | 320321,丰县,4,320300 |
| | | 320322,沛县,4,320300 |
| | | 320324,睢宁县,4,320300 |
| | | 320371,徐州经济技术开发区,4,320300 |
| | | 320381,新沂市,4,320300 |
| | | 320382,邳州市,4,320300 |
| | | 320402,天宁区,4,320400 |
| | | 320404,钟楼区,4,320400 |
| | | 320411,新北区,4,320400 |
| | | 320412,武进区,4,320400 |
| | | 320413,金坛区,4,320400 |
| | | 320481,溧阳市,4,320400 |
| | | 320505,虎丘区,4,320500 |
| | | 320506,吴中区,4,320500 |
| | | 320507,相城区,4,320500 |
| | | 320508,姑苏区,4,320500 |
| | | 320509,吴江区,4,320500 |
| | | 320571,苏州工业园区,4,320500 |
| | | 320581,常熟市,4,320500 |
| | | 320582,张家港市,4,320500 |
| | | 320583,昆山市,4,320500 |
| | | 320585,太仓市,4,320500 |
| | | 320612,通州区,4,320600 |
| | | 320613,崇川区,4,320600 |
| | | 320614,海门区,4,320600 |
| | | 320623,如东县,4,320600 |
| | | 320671,南通经济技术开发区,4,320600 |
| | | 320681,启东市,4,320600 |
| | | 320682,如皋市,4,320600 |
| | | 320685,海安市,4,320600 |
| | | 320703,连云区,4,320700 |
| | | 320706,海州区,4,320700 |
| | | 320707,赣榆区,4,320700 |
| | | 320722,东海县,4,320700 |
| | | 320723,灌云县,4,320700 |
| | | 320724,灌南县,4,320700 |
| | | 320771,连云港经济技术开发区,4,320700 |
| | | 320772,连云港高新技术产业开发区,4,320700 |
| | | 320803,淮安区,4,320800 |
| | | 320804,淮阴区,4,320800 |
| | | 320812,清江浦区,4,320800 |
| | | 320813,洪泽区,4,320800 |
| | | 320826,涟水县,4,320800 |
| | | 320830,盱眙县,4,320800 |
| | | 320831,金湖县,4,320800 |
| | | 320871,淮安经济技术开发区,4,320800 |
| | | 320902,亭湖区,4,320900 |
| | | 320903,盐都区,4,320900 |
| | | 320904,大丰区,4,320900 |
| | | 320921,响水县,4,320900 |
| | | 320922,滨海县,4,320900 |
| | | 320923,阜宁县,4,320900 |
| | | 320924,射阳县,4,320900 |
| | | 320925,建湖县,4,320900 |
| | | 320971,盐城经济技术开发区,4,320900 |
| | | 320981,东台市,4,320900 |
| | | 321002,广陵区,4,321000 |
| | | 321003,邗江区,4,321000 |
| | | 321012,江都区,4,321000 |
| | | 321023,宝应县,4,321000 |
| | | 321071,扬州经济技术开发区,4,321000 |
| | | 321081,仪征市,4,321000 |
| | | 321084,高邮市,4,321000 |
| | | 321102,京口区,4,321100 |
| | | 321111,润州区,4,321100 |
| | | 321112,丹徒区,4,321100 |
| | | 321171,镇江新区,4,321100 |
| | | 321181,丹阳市,4,321100 |
| | | 321182,扬中市,4,321100 |
| | | 321183,句容市,4,321100 |
| | | 321202,海陵区,4,321200 |
| | | 321203,高港区,4,321200 |
| | | 321204,姜堰区,4,321200 |
| | | 321271,泰州医药高新技术产业开发区,4,321200 |
| | | 321281,兴化市,4,321200 |
| | | 321282,靖江市,4,321200 |
| | | 321283,泰兴市,4,321200 |
| | | 321302,宿城区,4,321300 |
| | | 321311,宿豫区,4,321300 |
| | | 321322,沭阳县,4,321300 |
| | | 321323,泗阳县,4,321300 |
| | | 321324,泗洪县,4,321300 |
| | | 321371,宿迁经济技术开发区,4,321300 |
| | | 330102,上城区,4,330100 |
| | | 330105,拱墅区,4,330100 |
| | | 330106,西湖区,4,330100 |
| | | 330108,滨江区,4,330100 |
| | | 330109,萧山区,4,330100 |
| | | 330110,余杭区,4,330100 |
| | | 330111,富阳区,4,330100 |
| | | 330112,临安区,4,330100 |
| | | 330113,临平区,4,330100 |
| | | 330114,钱塘区,4,330100 |
| | | 330122,桐庐县,4,330100 |
| | | 330127,淳安县,4,330100 |
| | | 330182,建德市,4,330100 |
| | | 330203,海曙区,4,330200 |
| | | 330205,江北区,4,330200 |
| | | 330206,北仑区,4,330200 |
| | | 330211,镇海区,4,330200 |
| | | 330212,鄞州区,4,330200 |
| | | 330213,奉化区,4,330200 |
| | | 330225,象山县,4,330200 |
| | | 330226,宁海县,4,330200 |
| | | 330281,余姚市,4,330200 |
| | | 330282,慈溪市,4,330200 |
| | | 330302,鹿城区,4,330300 |
| | | 330303,龙湾区,4,330300 |
| | | 330304,瓯海区,4,330300 |
| | | 330305,洞头区,4,330300 |
| | | 330324,永嘉县,4,330300 |
| | | 330326,平阳县,4,330300 |
| | | 330327,苍南县,4,330300 |
| | | 330328,文成县,4,330300 |
| | | 330329,泰顺县,4,330300 |
| | | 330371,温州经济技术开发区,4,330300 |
| | | 330381,瑞安市,4,330300 |
| | | 330382,乐清市,4,330300 |
| | | 330383,龙港市,4,330300 |
| | | 330402,南湖区,4,330400 |
| | | 330411,秀洲区,4,330400 |
| | | 330421,嘉善县,4,330400 |
| | | 330424,海盐县,4,330400 |
| | | 330481,海宁市,4,330400 |
| | | 330482,平湖市,4,330400 |
| | | 330483,桐乡市,4,330400 |
| | | 330502,吴兴区,4,330500 |
| | | 330503,南浔区,4,330500 |
| | | 330521,德清县,4,330500 |
| | | 330522,长兴县,4,330500 |
| | | 330523,安吉县,4,330500 |
| | | 330602,越城区,4,330600 |
| | | 330603,柯桥区,4,330600 |
| | | 330604,上虞区,4,330600 |
| | | 330624,新昌县,4,330600 |
| | | 330681,诸暨市,4,330600 |
| | | 330683,嵊州市,4,330600 |
| | | 330702,婺城区,4,330700 |
| | | 330703,金东区,4,330700 |
| | | 330723,武义县,4,330700 |
| | | 330726,浦江县,4,330700 |
| | | 330727,磐安县,4,330700 |
| | | 330781,兰溪市,4,330700 |
| | | 330782,义乌市,4,330700 |
| | | 330783,东阳市,4,330700 |
| | | 330784,永康市,4,330700 |
| | | 330802,柯城区,4,330800 |
| | | 330803,衢江区,4,330800 |
| | | 330822,常山县,4,330800 |
| | | 330824,开化县,4,330800 |
| | | 330825,龙游县,4,330800 |
| | | 330881,江山市,4,330800 |
| | | 330902,定海区,4,330900 |
| | | 330903,普陀区,4,330900 |
| | | 330921,岱山县,4,330900 |
| | | 330922,嵊泗县,4,330900 |
| | | 331002,椒江区,4,331000 |
| | | 331003,黄岩区,4,331000 |
| | | 331004,路桥区,4,331000 |
| | | 331022,三门县,4,331000 |
| | | 331023,天台县,4,331000 |
| | | 331024,仙居县,4,331000 |
| | | 331081,温岭市,4,331000 |
| | | 331082,临海市,4,331000 |
| | | 331083,玉环市,4,331000 |
| | | 331102,莲都区,4,331100 |
| | | 331121,青田县,4,331100 |
| | | 331122,缙云县,4,331100 |
| | | 331123,遂昌县,4,331100 |
| | | 331124,松阳县,4,331100 |
| | | 331125,云和县,4,331100 |
| | | 331126,庆元县,4,331100 |
| | | 331127,景宁畲族自治县,4,331100 |
| | | 331181,龙泉市,4,331100 |
| | | 340102,瑶海区,4,340100 |
| | | 340103,庐阳区,4,340100 |
| | | 340104,蜀山区,4,340100 |
| | | 340111,包河区,4,340100 |
| | | 340121,长丰县,4,340100 |
| | | 340122,肥东县,4,340100 |
| | | 340123,肥西县,4,340100 |
| | | 340124,庐江县,4,340100 |
| | | 340171,合肥高新技术产业开发区,4,340100 |
| | | 340172,合肥经济技术开发区,4,340100 |
| | | 340173,合肥新站高新技术产业开发区,4,340100 |
| | | 340181,巢湖市,4,340100 |
| | | 340202,镜湖区,4,340200 |
| | | 340207,鸠江区,4,340200 |
| | | 340209,弋江区,4,340200 |
| | | 340210,湾沚区,4,340200 |
| | | 340212,繁昌区,4,340200 |
| | | 340223,南陵县,4,340200 |
| | | 340271,芜湖经济技术开发区,4,340200 |
| | | 340272,安徽芜湖三山经济开发区,4,340200 |
| | | 340281,无为市,4,340200 |
| | | 340302,龙子湖区,4,340300 |
| | | 340303,蚌山区,4,340300 |
| | | 340304,禹会区,4,340300 |
| | | 340311,淮上区,4,340300 |
| | | 340321,怀远县,4,340300 |
| | | 340322,五河县,4,340300 |
| | | 340323,固镇县,4,340300 |
| | | 340371,蚌埠市高新技术开发区,4,340300 |
| | | 340372,蚌埠市经济开发区,4,340300 |
| | | 340402,大通区,4,340400 |
| | | 340403,田家庵区,4,340400 |
| | | 340404,谢家集区,4,340400 |
| | | 340405,八公山区,4,340400 |
| | | 340406,潘集区,4,340400 |
| | | 340421,凤台县,4,340400 |
| | | 340422,寿县,4,340400 |
| | | 340503,花山区,4,340500 |
| | | 340504,雨山区,4,340500 |
| | | 340506,博望区,4,340500 |
| | | 340521,当涂县,4,340500 |
| | | 340522,含山县,4,340500 |
| | | 340523,和县,4,340500 |
| | | 340602,杜集区,4,340600 |
| | | 340603,相山区,4,340600 |
| | | 340604,烈山区,4,340600 |
| | | 340621,濉溪县,4,340600 |
| | | 340705,铜官区,4,340700 |
| | | 340706,义安区,4,340700 |
| | | 340711,郊区,4,340700 |
| | | 340722,枞阳县,4,340700 |
| | | 340802,迎江区,4,340800 |
| | | 340803,大观区,4,340800 |
| | | 340811,宜秀区,4,340800 |
| | | 340822,怀宁县,4,340800 |
| | | 340825,太湖县,4,340800 |
| | | 340826,宿松县,4,340800 |
| | | 340827,望江县,4,340800 |
| | | 340828,岳西县,4,340800 |
| | | 340871,安徽安庆经济开发区,4,340800 |
| | | 340881,桐城市,4,340800 |
| | | 340882,潜山市,4,340800 |
| | | 341002,屯溪区,4,341000 |
| | | 341003,黄山区,4,341000 |
| | | 341004,徽州区,4,341000 |
| | | 341021,歙县,4,341000 |
| | | 341022,休宁县,4,341000 |
| | | 341023,黟县,4,341000 |
| | | 341024,祁门县,4,341000 |
| | | 341102,琅琊区,4,341100 |
| | | 341103,南谯区,4,341100 |
| | | 341122,来安县,4,341100 |
| | | 341124,全椒县,4,341100 |
| | | 341125,定远县,4,341100 |
| | | 341126,凤阳县,4,341100 |
| | | 341171,中新苏滁高新技术产业开发区,4,341100 |
| | | 341172,滁州经济技术开发区,4,341100 |
| | | 341181,天长市,4,341100 |
| | | 341182,明光市,4,341100 |
| | | 341202,颍州区,4,341200 |
| | | 341203,颍东区,4,341200 |
| | | 341204,颍泉区,4,341200 |
| | | 341221,临泉县,4,341200 |
| | | 341222,太和县,4,341200 |
| | | 341225,阜南县,4,341200 |
| | | 341226,颍上县,4,341200 |
| | | 341271,阜阳合肥现代产业园区,4,341200 |
| | | 341272,阜阳经济技术开发区,4,341200 |
| | | 341282,界首市,4,341200 |
| | | 341302,埇桥区,4,341300 |
| | | 341321,砀山县,4,341300 |
| | | 341322,萧县,4,341300 |
| | | 341323,灵璧县,4,341300 |
| | | 341324,泗县,4,341300 |
| | | 341371,宿州马鞍山现代产业园区,4,341300 |
| | | 341372,宿州经济技术开发区,4,341300 |
| | | 341502,金安区,4,341500 |
| | | 341503,裕安区,4,341500 |
| | | 341504,叶集区,4,341500 |
| | | 341522,霍邱县,4,341500 |
| | | 341523,舒城县,4,341500 |
| | | 341524,金寨县,4,341500 |
| | | 341525,霍山县,4,341500 |
| | | 341602,谯城区,4,341600 |
| | | 341621,涡阳县,4,341600 |
| | | 341622,蒙城县,4,341600 |
| | | 341623,利辛县,4,341600 |
| | | 341702,贵池区,4,341700 |
| | | 341721,东至县,4,341700 |
| | | 341722,石台县,4,341700 |
| | | 341723,青阳县,4,341700 |
| | | 341802,宣州区,4,341800 |
| | | 341821,郎溪县,4,341800 |
| | | 341823,泾县,4,341800 |
| | | 341824,绩溪县,4,341800 |
| | | 341825,旌德县,4,341800 |
| | | 341871,宣城市经济开发区,4,341800 |
| | | 341881,宁国市,4,341800 |
| | | 341882,广德市,4,341800 |
| | | 350102,鼓楼区,4,350100 |
| | | 350103,台江区,4,350100 |
| | | 350104,仓山区,4,350100 |
| | | 350105,马尾区,4,350100 |
| | | 350111,晋安区,4,350100 |
| | | 350112,长乐区,4,350100 |
| | | 350121,闽侯县,4,350100 |
| | | 350122,连江县,4,350100 |
| | | 350123,罗源县,4,350100 |
| | | 350124,闽清县,4,350100 |
| | | 350125,永泰县,4,350100 |
| | | 350128,平潭县,4,350100 |
| | | 350181,福清市,4,350100 |
| | | 350203,思明区,4,350200 |
| | | 350205,海沧区,4,350200 |
| | | 350206,湖里区,4,350200 |
| | | 350211,集美区,4,350200 |
| | | 350212,同安区,4,350200 |
| | | 350213,翔安区,4,350200 |
| | | 350302,城厢区,4,350300 |
| | | 350303,涵江区,4,350300 |
| | | 350304,荔城区,4,350300 |
| | | 350305,秀屿区,4,350300 |
| | | 350322,仙游县,4,350300 |
| | | 350404,三元区,4,350400 |
| | | 350405,沙县区,4,350400 |
| | | 350421,明溪县,4,350400 |
| | | 350423,清流县,4,350400 |
| | | 350424,宁化县,4,350400 |
| | | 350425,大田县,4,350400 |
| | | 350426,尤溪县,4,350400 |
| | | 350428,将乐县,4,350400 |
| | | 350429,泰宁县,4,350400 |
| | | 350430,建宁县,4,350400 |
| | | 350481,永安市,4,350400 |
| | | 350502,鲤城区,4,350500 |
| | | 350503,丰泽区,4,350500 |
| | | 350504,洛江区,4,350500 |
| | | 350505,泉港区,4,350500 |
| | | 350521,惠安县,4,350500 |
| | | 350524,安溪县,4,350500 |
| | | 350525,永春县,4,350500 |
| | | 350526,德化县,4,350500 |
| | | 350527,金门县,4,350500 |
| | | 350581,石狮市,4,350500 |
| | | 350582,晋江市,4,350500 |
| | | 350583,南安市,4,350500 |
| | | 350602,芗城区,4,350600 |
| | | 350603,龙文区,4,350600 |
| | | 350604,龙海区,4,350600 |
| | | 350605,长泰区,4,350600 |
| | | 350622,云霄县,4,350600 |
| | | 350623,漳浦县,4,350600 |
| | | 350624,诏安县,4,350600 |
| | | 350626,东山县,4,350600 |
| | | 350627,南靖县,4,350600 |
| | | 350628,平和县,4,350600 |
| | | 350629,华安县,4,350600 |
| | | 350702,延平区,4,350700 |
| | | 350703,建阳区,4,350700 |
| | | 350721,顺昌县,4,350700 |
| | | 350722,浦城县,4,350700 |
| | | 350723,光泽县,4,350700 |
| | | 350724,松溪县,4,350700 |
| | | 350725,政和县,4,350700 |
| | | 350781,邵武市,4,350700 |
| | | 350782,武夷山市,4,350700 |
| | | 350783,建瓯市,4,350700 |
| | | 350802,新罗区,4,350800 |
| | | 350803,永定区,4,350800 |
| | | 350821,长汀县,4,350800 |
| | | 350823,上杭县,4,350800 |
| | | 350824,武平县,4,350800 |
| | | 350825,连城县,4,350800 |
| | | 350881,漳平市,4,350800 |
| | | 350902,蕉城区,4,350900 |
| | | 350921,霞浦县,4,350900 |
| | | 350922,古田县,4,350900 |
| | | 350923,屏南县,4,350900 |
| | | 350924,寿宁县,4,350900 |
| | | 350925,周宁县,4,350900 |
| | | 350926,柘荣县,4,350900 |
| | | 350981,福安市,4,350900 |
| | | 350982,福鼎市,4,350900 |
| | | 360102,东湖区,4,360100 |
| | | 360103,西湖区,4,360100 |
| | | 360104,青云谱区,4,360100 |
| | | 360111,青山湖区,4,360100 |
| | | 360112,新建区,4,360100 |
| | | 360113,红谷滩区,4,360100 |
| | | 360121,南昌县,4,360100 |
| | | 360123,安义县,4,360100 |
| | | 360124,进贤县,4,360100 |
| | | 360202,昌江区,4,360200 |
| | | 360203,珠山区,4,360200 |
| | | 360222,浮梁县,4,360200 |
| | | 360281,乐平市,4,360200 |
| | | 360302,安源区,4,360300 |
| | | 360313,湘东区,4,360300 |
| | | 360321,莲花县,4,360300 |
| | | 360322,上栗县,4,360300 |
| | | 360323,芦溪县,4,360300 |
| | | 360402,濂溪区,4,360400 |
| | | 360403,浔阳区,4,360400 |
| | | 360404,柴桑区,4,360400 |
| | | 360423,武宁县,4,360400 |
| | | 360424,修水县,4,360400 |
| | | 360425,永修县,4,360400 |
| | | 360426,德安县,4,360400 |
| | | 360428,都昌县,4,360400 |
| | | 360429,湖口县,4,360400 |
| | | 360430,彭泽县,4,360400 |
| | | 360481,瑞昌市,4,360400 |
| | | 360482,共青城市,4,360400 |
| | | 360483,庐山市,4,360400 |
| | | 360502,渝水区,4,360500 |
| | | 360521,分宜县,4,360500 |
| | | 360602,月湖区,4,360600 |
| | | 360603,余江区,4,360600 |
| | | 360681,贵溪市,4,360600 |
| | | 360702,章贡区,4,360700 |
| | | 360703,南康区,4,360700 |
| | | 360704,赣县区,4,360700 |
| | | 360722,信丰县,4,360700 |
| | | 360723,大余县,4,360700 |
| | | 360724,上犹县,4,360700 |
| | | 360725,崇义县,4,360700 |
| | | 360726,安远县,4,360700 |
| | | 360728,定南县,4,360700 |
| | | 360729,全南县,4,360700 |
| | | 360730,宁都县,4,360700 |
| | | 360731,于都县,4,360700 |
| | | 360732,兴国县,4,360700 |
| | | 360733,会昌县,4,360700 |
| | | 360734,寻乌县,4,360700 |
| | | 360735,石城县,4,360700 |
| | | 360781,瑞金市,4,360700 |
| | | 360783,龙南市,4,360700 |
| | | 360802,吉州区,4,360800 |
| | | 360803,青原区,4,360800 |
| | | 360821,吉安县,4,360800 |
| | | 360822,吉水县,4,360800 |
| | | 360823,峡江县,4,360800 |
| | | 360824,新干县,4,360800 |
| | | 360825,永丰县,4,360800 |
| | | 360826,泰和县,4,360800 |
| | | 360827,遂川县,4,360800 |
| | | 360828,万安县,4,360800 |
| | | 360829,安福县,4,360800 |
| | | 360830,永新县,4,360800 |
| | | 360881,井冈山市,4,360800 |
| | | 360902,袁州区,4,360900 |
| | | 360921,奉新县,4,360900 |
| | | 360922,万载县,4,360900 |
| | | 360923,上高县,4,360900 |
| | | 360924,宜丰县,4,360900 |
| | | 360925,靖安县,4,360900 |
| | | 360926,铜鼓县,4,360900 |
| | | 360981,丰城市,4,360900 |
| | | 360982,樟树市,4,360900 |
| | | 360983,高安市,4,360900 |
| | | 361002,临川区,4,361000 |
| | | 361003,东乡区,4,361000 |
| | | 361021,南城县,4,361000 |
| | | 361022,黎川县,4,361000 |
| | | 361023,南丰县,4,361000 |
| | | 361024,崇仁县,4,361000 |
| | | 361025,乐安县,4,361000 |
| | | 361026,宜黄县,4,361000 |
| | | 361027,金溪县,4,361000 |
| | | 361028,资溪县,4,361000 |
| | | 361030,广昌县,4,361000 |
| | | 361102,信州区,4,361100 |
| | | 361103,广丰区,4,361100 |
| | | 361104,广信区,4,361100 |
| | | 361123,玉山县,4,361100 |
| | | 361124,铅山县,4,361100 |
| | | 361125,横峰县,4,361100 |
| | | 361126,弋阳县,4,361100 |
| | | 361127,余干县,4,361100 |
| | | 361128,鄱阳县,4,361100 |
| | | 361129,万年县,4,361100 |
| | | 361130,婺源县,4,361100 |
| | | 361181,德兴市,4,361100 |
| | | 370102,历下区,4,370100 |
| | | 370103,市中区,4,370100 |
| | | 370104,槐荫区,4,370100 |
| | | 370105,天桥区,4,370100 |
| | | 370112,历城区,4,370100 |
| | | 370113,长清区,4,370100 |
| | | 370114,章丘区,4,370100 |
| | | 370115,济阳区,4,370100 |
| | | 370116,莱芜区,4,370100 |
| | | 370117,钢城区,4,370100 |
| | | 370124,平阴县,4,370100 |
| | | 370126,商河县,4,370100 |
| | | 370171,济南高新技术产业开发区,4,370100 |
| | | 370202,市南区,4,370200 |
| | | 370203,市北区,4,370200 |
| | | 370211,黄岛区,4,370200 |
| | | 370212,崂山区,4,370200 |
| | | 370213,李沧区,4,370200 |
| | | 370214,城阳区,4,370200 |
| | | 370215,即墨区,4,370200 |
| | | 370271,青岛高新技术产业开发区,4,370200 |
| | | 370281,胶州市,4,370200 |
| | | 370283,平度市,4,370200 |
| | | 370285,莱西市,4,370200 |
| | | 370302,淄川区,4,370300 |
| | | 370303,张店区,4,370300 |
| | | 370304,博山区,4,370300 |
| | | 370305,临淄区,4,370300 |
| | | 370306,周村区,4,370300 |
| | | 370321,桓台县,4,370300 |
| | | 370322,高青县,4,370300 |
| | | 370323,沂源县,4,370300 |
| | | 370402,市中区,4,370400 |
| | | 370403,薛城区,4,370400 |
| | | 370404,峄城区,4,370400 |
| | | 370405,台儿庄区,4,370400 |
| | | 370406,山亭区,4,370400 |
| | | 370481,滕州市,4,370400 |
| | | 370502,东营区,4,370500 |
| | | 370503,河口区,4,370500 |
| | | 370505,垦利区,4,370500 |
| | | 370522,利津县,4,370500 |
| | | 370523,广饶县,4,370500 |
| | | 370571,东营经济技术开发区,4,370500 |
| | | 370572,东营港经济开发区,4,370500 |
| | | 370602,芝罘区,4,370600 |
| | | 370611,福山区,4,370600 |
| | | 370612,牟平区,4,370600 |
| | | 370613,莱山区,4,370600 |
| | | 370614,蓬莱区,4,370600 |
| | | 370671,烟台高新技术产业开发区,4,370600 |
| | | 370672,烟台经济技术开发区,4,370600 |
| | | 370681,龙口市,4,370600 |
| | | 370682,莱阳市,4,370600 |
| | | 370683,莱州市,4,370600 |
| | | 370685,招远市,4,370600 |
| | | 370686,栖霞市,4,370600 |
| | | 370687,海阳市,4,370600 |
| | | 370702,潍城区,4,370700 |
| | | 370703,寒亭区,4,370700 |
| | | 370704,坊子区,4,370700 |
| | | 370705,奎文区,4,370700 |
| | | 370724,临朐县,4,370700 |
| | | 370725,昌乐县,4,370700 |
| | | 370772,潍坊滨海经济技术开发区,4,370700 |
| | | 370781,青州市,4,370700 |
| | | 370782,诸城市,4,370700 |
| | | 370783,寿光市,4,370700 |
| | | 370784,安丘市,4,370700 |
| | | 370785,高密市,4,370700 |
| | | 370786,昌邑市,4,370700 |
| | | 370811,任城区,4,370800 |
| | | 370812,兖州区,4,370800 |
| | | 370826,微山县,4,370800 |
| | | 370827,鱼台县,4,370800 |
| | | 370828,金乡县,4,370800 |
| | | 370829,嘉祥县,4,370800 |
| | | 370830,汶上县,4,370800 |
| | | 370831,泗水县,4,370800 |
| | | 370832,梁山县,4,370800 |
| | | 370871,济宁高新技术产业开发区,4,370800 |
| | | 370881,曲阜市,4,370800 |
| | | 370883,邹城市,4,370800 |
| | | 370902,泰山区,4,370900 |
| | | 370911,岱岳区,4,370900 |
| | | 370921,宁阳县,4,370900 |
| | | 370923,东平县,4,370900 |
| | | 370982,新泰市,4,370900 |
| | | 370983,肥城市,4,370900 |
| | | 371002,环翠区,4,371000 |
| | | 371003,文登区,4,371000 |
| | | 371071,威海火炬高技术产业开发区,4,371000 |
| | | 371072,威海经济技术开发区,4,371000 |
| | | 371073,威海临港经济技术开发区,4,371000 |
| | | 371082,荣成市,4,371000 |
| | | 371083,乳山市,4,371000 |
| | | 371102,东港区,4,371100 |
| | | 371103,岚山区,4,371100 |
| | | 371121,五莲县,4,371100 |
| | | 371122,莒县,4,371100 |
| | | 371171,日照经济技术开发区,4,371100 |
| | | 371302,兰山区,4,371300 |
| | | 371311,罗庄区,4,371300 |
| | | 371312,河东区,4,371300 |
| | | 371321,沂南县,4,371300 |
| | | 371322,郯城县,4,371300 |
| | | 371323,沂水县,4,371300 |
| | | 371324,兰陵县,4,371300 |
| | | 371325,费县,4,371300 |
| | | 371326,平邑县,4,371300 |
| | | 371327,莒南县,4,371300 |
| | | 371328,蒙阴县,4,371300 |
| | | 371329,临沭县,4,371300 |
| | | 371371,临沂高新技术产业开发区,4,371300 |
| | | 371402,德城区,4,371400 |
| | | 371403,陵城区,4,371400 |
| | | 371422,宁津县,4,371400 |
| | | 371423,庆云县,4,371400 |
| | | 371424,临邑县,4,371400 |
| | | 371425,齐河县,4,371400 |
| | | 371426,平原县,4,371400 |
| | | 371427,夏津县,4,371400 |
| | | 371428,武城县,4,371400 |
| | | 371471,德州经济技术开发区,4,371400 |
| | | 371472,德州运河经济开发区,4,371400 |
| | | 371481,乐陵市,4,371400 |
| | | 371482,禹城市,4,371400 |
| | | 371502,东昌府区,4,371500 |
| | | 371503,茌平区,4,371500 |
| | | 371521,阳谷县,4,371500 |
| | | 371522,莘县,4,371500 |
| | | 371524,东阿县,4,371500 |
| | | 371525,冠县,4,371500 |
| | | 371526,高唐县,4,371500 |
| | | 371581,临清市,4,371500 |
| | | 371602,滨城区,4,371600 |
| | | 371603,沾化区,4,371600 |
| | | 371621,惠民县,4,371600 |
| | | 371622,阳信县,4,371600 |
| | | 371623,无棣县,4,371600 |
| | | 371625,博兴县,4,371600 |
| | | 371681,邹平市,4,371600 |
| | | 371702,牡丹区,4,371700 |
| | | 371703,定陶区,4,371700 |
| | | 371721,曹县,4,371700 |
| | | 371722,单县,4,371700 |
| | | 371723,成武县,4,371700 |
| | | 371724,巨野县,4,371700 |
| | | 371725,郓城县,4,371700 |
| | | 371726,鄄城县,4,371700 |
| | | 371728,东明县,4,371700 |
| | | 371771,菏泽经济技术开发区,4,371700 |
| | | 371772,菏泽高新技术开发区,4,371700 |
| | | 410102,中原区,4,410100 |
| | | 410103,二七区,4,410100 |
| | | 410104,管城回族区,4,410100 |
| | | 410105,金水区,4,410100 |
| | | 410106,上街区,4,410100 |
| | | 410108,惠济区,4,410100 |
| | | 410122,中牟县,4,410100 |
| | | 410171,郑州经济技术开发区,4,410100 |
| | | 410172,郑州高新技术产业开发区,4,410100 |
| | | 410173,郑州航空港经济综合实验区,4,410100 |
| | | 410181,巩义市,4,410100 |
| | | 410182,荥阳市,4,410100 |
| | | 410183,新密市,4,410100 |
| | | 410184,新郑市,4,410100 |
| | | 410185,登封市,4,410100 |
| | | 410202,龙亭区,4,410200 |
| | | 410203,顺河回族区,4,410200 |
| | | 410204,鼓楼区,4,410200 |
| | | 410205,禹王台区,4,410200 |
| | | 410212,祥符区,4,410200 |
| | | 410221,杞县,4,410200 |
| | | 410222,通许县,4,410200 |
| | | 410223,尉氏县,4,410200 |
| | | 410225,兰考县,4,410200 |
| | | 410302,老城区,4,410300 |
| | | 410303,西工区,4,410300 |
| | | 410304,瀍河回族区,4,410300 |
| | | 410305,涧西区,4,410300 |
| | | 410307,偃师区,4,410300 |
| | | 410308,孟津区,4,410300 |
| | | 410311,洛龙区,4,410300 |
| | | 410323,新安县,4,410300 |
| | | 410324,栾川县,4,410300 |
| | | 410325,嵩县,4,410300 |
| | | 410326,汝阳县,4,410300 |
| | | 410327,宜阳县,4,410300 |
| | | 410328,洛宁县,4,410300 |
| | | 410329,伊川县,4,410300 |
| | | 410371,洛阳高新技术产业开发区,4,410300 |
| | | 410402,新华区,4,410400 |
| | | 410403,卫东区,4,410400 |
| | | 410404,石龙区,4,410400 |
| | | 410411,湛河区,4,410400 |
| | | 410421,宝丰县,4,410400 |
| | | 410422,叶县,4,410400 |
| | | 410423,鲁山县,4,410400 |
| | | 410425,郏县,4,410400 |
| | | 410471,平顶山高新技术产业开发区,4,410400 |
| | | 410472,平顶山市城乡一体化示范区,4,410400 |
| | | 410481,舞钢市,4,410400 |
| | | 410482,汝州市,4,410400 |
| | | 410502,文峰区,4,410500 |
| | | 410503,北关区,4,410500 |
| | | 410505,殷都区,4,410500 |
| | | 410506,龙安区,4,410500 |
| | | 410522,安阳县,4,410500 |
| | | 410523,汤阴县,4,410500 |
| | | 410526,滑县,4,410500 |
| | | 410527,内黄县,4,410500 |
| | | 410571,安阳高新技术产业开发区,4,410500 |
| | | 410581,林州市,4,410500 |
| | | 410602,鹤山区,4,410600 |
| | | 410603,山城区,4,410600 |
| | | 410611,淇滨区,4,410600 |
| | | 410621,浚县,4,410600 |
| | | 410622,淇县,4,410600 |
| | | 410671,鹤壁经济技术开发区,4,410600 |
| | | 410702,红旗区,4,410700 |
| | | 410703,卫滨区,4,410700 |
| | | 410704,凤泉区,4,410700 |
| | | 410711,牧野区,4,410700 |
| | | 410721,新乡县,4,410700 |
| | | 410724,获嘉县,4,410700 |
| | | 410725,原阳县,4,410700 |
| | | 410726,延津县,4,410700 |
| | | 410727,封丘县,4,410700 |
| | | 410771,新乡高新技术产业开发区,4,410700 |
| | | 410772,新乡经济技术开发区,4,410700 |
| | | 410773,新乡市平原城乡一体化示范区,4,410700 |
| | | 410781,卫辉市,4,410700 |
| | | 410782,辉县市,4,410700 |
| | | 410783,长垣市,4,410700 |
| | | 410802,解放区,4,410800 |
| | | 410803,中站区,4,410800 |
| | | 410804,马村区,4,410800 |
| | | 410811,山阳区,4,410800 |
| | | 410821,修武县,4,410800 |
| | | 410822,博爱县,4,410800 |
| | | 410823,武陟县,4,410800 |
| | | 410825,温县,4,410800 |
| | | 410871,焦作城乡一体化示范区,4,410800 |
| | | 410882,沁阳市,4,410800 |
| | | 410883,孟州市,4,410800 |
| | | 410902,华龙区,4,410900 |
| | | 410922,清丰县,4,410900 |
| | | 410923,南乐县,4,410900 |
| | | 410926,范县,4,410900 |
| | | 410927,台前县,4,410900 |
| | | 410928,濮阳县,4,410900 |
| | | 410971,河南濮阳工业园区,4,410900 |
| | | 410972,濮阳经济技术开发区,4,410900 |
| | | 411002,魏都区,4,411000 |
| | | 411003,建安区,4,411000 |
| | | 411024,鄢陵县,4,411000 |
| | | 411025,襄城县,4,411000 |
| | | 411071,许昌经济技术开发区,4,411000 |
| | | 411081,禹州市,4,411000 |
| | | 411082,长葛市,4,411000 |
| | | 411102,源汇区,4,411100 |
| | | 411103,郾城区,4,411100 |
| | | 411104,召陵区,4,411100 |
| | | 411121,舞阳县,4,411100 |
| | | 411122,临颍县,4,411100 |
| | | 411171,漯河经济技术开发区,4,411100 |
| | | 411202,湖滨区,4,411200 |
| | | 411203,陕州区,4,411200 |
| | | 411221,渑池县,4,411200 |
| | | 411224,卢氏县,4,411200 |
| | | 411271,河南三门峡经济开发区,4,411200 |
| | | 411281,义马市,4,411200 |
| | | 411282,灵宝市,4,411200 |
| | | 411302,宛城区,4,411300 |
| | | 411303,卧龙区,4,411300 |
| | | 411321,南召县,4,411300 |
| | | 411322,方城县,4,411300 |
| | | 411323,西峡县,4,411300 |
| | | 411324,镇平县,4,411300 |
| | | 411325,内乡县,4,411300 |
| | | 411326,淅川县,4,411300 |
| | | 411327,社旗县,4,411300 |
| | | 411328,唐河县,4,411300 |
| | | 411329,新野县,4,411300 |
| | | 411330,桐柏县,4,411300 |
| | | 411371,南阳高新技术产业开发区,4,411300 |
| | | 411372,南阳市城乡一体化示范区,4,411300 |
| | | 411381,邓州市,4,411300 |
| | | 411402,梁园区,4,411400 |
| | | 411403,睢阳区,4,411400 |
| | | 411421,民权县,4,411400 |
| | | 411422,睢县,4,411400 |
| | | 411423,宁陵县,4,411400 |
| | | 411424,柘城县,4,411400 |
| | | 411425,虞城县,4,411400 |
| | | 411426,夏邑县,4,411400 |
| | | 411471,豫东综合物流产业聚集区,4,411400 |
| | | 411472,河南商丘经济开发区,4,411400 |
| | | 411481,永城市,4,411400 |
| | | 411502,浉河区,4,411500 |
| | | 411503,平桥区,4,411500 |
| | | 411521,罗山县,4,411500 |
| | | 411522,光山县,4,411500 |
| | | 411523,新县,4,411500 |
| | | 411524,商城县,4,411500 |
| | | 411525,固始县,4,411500 |
| | | 411526,潢川县,4,411500 |
| | | 411527,淮滨县,4,411500 |
| | | 411528,息县,4,411500 |
| | | 411571,信阳高新技术产业开发区,4,411500 |
| | | 411602,川汇区,4,411600 |
| | | 411603,淮阳区,4,411600 |
| | | 411621,扶沟县,4,411600 |
| | | 411622,西华县,4,411600 |
| | | 411623,商水县,4,411600 |
| | | 411624,沈丘县,4,411600 |
| | | 411625,郸城县,4,411600 |
| | | 411627,太康县,4,411600 |
| | | 411628,鹿邑县,4,411600 |
| | | 411671,河南周口经济开发区,4,411600 |
| | | 411681,项城市,4,411600 |
| | | 411702,驿城区,4,411700 |
| | | 411721,西平县,4,411700 |
| | | 411722,上蔡县,4,411700 |
| | | 411723,平舆县,4,411700 |
| | | 411724,正阳县,4,411700 |
| | | 411725,确山县,4,411700 |
| | | 411726,泌阳县,4,411700 |
| | | 411727,汝南县,4,411700 |
| | | 411728,遂平县,4,411700 |
| | | 411729,新蔡县,4,411700 |
| | | 411771,河南驻马店经济开发区,4,411700 |
| | | 419001,济源市,4,419000 |
| | | 420102,江岸区,4,420100 |
| | | 420103,江汉区,4,420100 |
| | | 420104,硚口区,4,420100 |
| | | 420105,汉阳区,4,420100 |
| | | 420106,武昌区,4,420100 |
| | | 420107,青山区,4,420100 |
| | | 420111,洪山区,4,420100 |
| | | 420112,东西湖区,4,420100 |
| | | 420113,汉南区,4,420100 |
| | | 420114,蔡甸区,4,420100 |
| | | 420115,江夏区,4,420100 |
| | | 420116,黄陂区,4,420100 |
| | | 420117,新洲区,4,420100 |
| | | 420202,黄石港区,4,420200 |
| | | 420203,西塞山区,4,420200 |
| | | 420204,下陆区,4,420200 |
| | | 420205,铁山区,4,420200 |
| | | 420222,阳新县,4,420200 |
| | | 420281,大冶市,4,420200 |
| | | 420302,茅箭区,4,420300 |
| | | 420303,张湾区,4,420300 |
| | | 420304,郧阳区,4,420300 |
| | | 420322,郧西县,4,420300 |
| | | 420323,竹山县,4,420300 |
| | | 420324,竹溪县,4,420300 |
| | | 420325,房县,4,420300 |
| | | 420381,丹江口市,4,420300 |
| | | 420502,西陵区,4,420500 |
| | | 420503,伍家岗区,4,420500 |
| | | 420504,点军区,4,420500 |
| | | 420505,猇亭区,4,420500 |
| | | 420506,夷陵区,4,420500 |
| | | 420525,远安县,4,420500 |
| | | 420526,兴山县,4,420500 |
| | | 420527,秭归县,4,420500 |
| | | 420528,长阳土家族自治县,4,420500 |
| | | 420529,五峰土家族自治县,4,420500 |
| | | 420581,宜都市,4,420500 |
| | | 420582,当阳市,4,420500 |
| | | 420583,枝江市,4,420500 |
| | | 420602,襄城区,4,420600 |
| | | 420606,樊城区,4,420600 |
| | | 420607,襄州区,4,420600 |
| | | 420624,南漳县,4,420600 |
| | | 420625,谷城县,4,420600 |
| | | 420626,保康县,4,420600 |
| | | 420682,老河口市,4,420600 |
| | | 420683,枣阳市,4,420600 |
| | | 420684,宜城市,4,420600 |
| | | 420702,梁子湖区,4,420700 |
| | | 420703,华容区,4,420700 |
| | | 420704,鄂城区,4,420700 |
| | | 420802,东宝区,4,420800 |
| | | 420804,掇刀区,4,420800 |
| | | 420822,沙洋县,4,420800 |
| | | 420881,钟祥市,4,420800 |
| | | 420882,京山市,4,420800 |
| | | 420902,孝南区,4,420900 |
| | | 420921,孝昌县,4,420900 |
| | | 420922,大悟县,4,420900 |
| | | 420923,云梦县,4,420900 |
| | | 420981,应城市,4,420900 |
| | | 420982,安陆市,4,420900 |
| | | 420984,汉川市,4,420900 |
| | | 421002,沙市区,4,421000 |
| | | 421003,荆州区,4,421000 |
| | | 421022,公安县,4,421000 |
| | | 421024,江陵县,4,421000 |
| | | 421071,荆州经济技术开发区,4,421000 |
| | | 421081,石首市,4,421000 |
| | | 421083,洪湖市,4,421000 |
| | | 421087,松滋市,4,421000 |
| | | 421088,监利市,4,421000 |
| | | 421102,黄州区,4,421100 |
| | | 421121,团风县,4,421100 |
| | | 421122,红安县,4,421100 |
| | | 421123,罗田县,4,421100 |
| | | 421124,英山县,4,421100 |
| | | 421125,浠水县,4,421100 |
| | | 421126,蕲春县,4,421100 |
| | | 421127,黄梅县,4,421100 |
| | | 421171,龙感湖管理区,4,421100 |
| | | 421181,麻城市,4,421100 |
| | | 421182,武穴市,4,421100 |
| | | 421202,咸安区,4,421200 |
| | | 421221,嘉鱼县,4,421200 |
| | | 421222,通城县,4,421200 |
| | | 421223,崇阳县,4,421200 |
| | | 421224,通山县,4,421200 |
| | | 421281,赤壁市,4,421200 |
| | | 421303,曾都区,4,421300 |
| | | 421321,随县,4,421300 |
| | | 421381,广水市,4,421300 |
| | | 422801,恩施市,4,422800 |
| | | 422802,利川市,4,422800 |
| | | 422822,建始县,4,422800 |
| | | 422823,巴东县,4,422800 |
| | | 422825,宣恩县,4,422800 |
| | | 422826,咸丰县,4,422800 |
| | | 422827,来凤县,4,422800 |
| | | 422828,鹤峰县,4,422800 |
| | | 429004,仙桃市,4,429000 |
| | | 429005,潜江市,4,429000 |
| | | 429006,天门市,4,429000 |
| | | 429021,神农架林区,4,429000 |
| | | 430102,芙蓉区,4,430100 |
| | | 430103,天心区,4,430100 |
| | | 430104,岳麓区,4,430100 |
| | | 430105,开福区,4,430100 |
| | | 430111,雨花区,4,430100 |
| | | 430112,望城区,4,430100 |
| | | 430121,长沙县,4,430100 |
| | | 430181,浏阳市,4,430100 |
| | | 430182,宁乡市,4,430100 |
| | | 430202,荷塘区,4,430200 |
| | | 430203,芦淞区,4,430200 |
| | | 430204,石峰区,4,430200 |
| | | 430211,天元区,4,430200 |
| | | 430212,渌口区,4,430200 |
| | | 430223,攸县,4,430200 |
| | | 430224,茶陵县,4,430200 |
| | | 430225,炎陵县,4,430200 |
| | | 430271,云龙示范区,4,430200 |
| | | 430281,醴陵市,4,430200 |
| | | 430302,雨湖区,4,430300 |
| | | 430304,岳塘区,4,430300 |
| | | 430321,湘潭县,4,430300 |
| | | 430371,湖南湘潭高新技术产业园区,4,430300 |
| | | 430372,湘潭昭山示范区,4,430300 |
| | | 430373,湘潭九华示范区,4,430300 |
| | | 430381,湘乡市,4,430300 |
| | | 430382,韶山市,4,430300 |
| | | 430405,珠晖区,4,430400 |
| | | 430406,雁峰区,4,430400 |
| | | 430407,石鼓区,4,430400 |
| | | 430408,蒸湘区,4,430400 |
| | | 430412,南岳区,4,430400 |
| | | 430421,衡阳县,4,430400 |
| | | 430422,衡南县,4,430400 |
| | | 430423,衡山县,4,430400 |
| | | 430424,衡东县,4,430400 |
| | | 430426,祁东县,4,430400 |
| | | 430471,衡阳综合保税区,4,430400 |
| | | 430472,湖南衡阳高新技术产业园区,4,430400 |
| | | 430473,湖南衡阳松木经济开发区,4,430400 |
| | | 430481,耒阳市,4,430400 |
| | | 430482,常宁市,4,430400 |
| | | 430502,双清区,4,430500 |
| | | 430503,大祥区,4,430500 |
| | | 430511,北塔区,4,430500 |
| | | 430522,新邵县,4,430500 |
| | | 430523,邵阳县,4,430500 |
| | | 430524,隆回县,4,430500 |
| | | 430525,洞口县,4,430500 |
| | | 430527,绥宁县,4,430500 |
| | | 430528,新宁县,4,430500 |
| | | 430529,城步苗族自治县,4,430500 |
| | | 430581,武冈市,4,430500 |
| | | 430582,邵东市,4,430500 |
| | | 430602,岳阳楼区,4,430600 |
| | | 430603,云溪区,4,430600 |
| | | 430611,君山区,4,430600 |
| | | 430621,岳阳县,4,430600 |
| | | 430623,华容县,4,430600 |
| | | 430624,湘阴县,4,430600 |
| | | 430626,平江县,4,430600 |
| | | 430671,岳阳市屈原管理区,4,430600 |
| | | 430681,汨罗市,4,430600 |
| | | 430682,临湘市,4,430600 |
| | | 430702,武陵区,4,430700 |
| | | 430703,鼎城区,4,430700 |
| | | 430721,安乡县,4,430700 |
| | | 430722,汉寿县,4,430700 |
| | | 430723,澧县,4,430700 |
| | | 430724,临澧县,4,430700 |
| | | 430725,桃源县,4,430700 |
| | | 430726,石门县,4,430700 |
| | | 430771,常德市西洞庭管理区,4,430700 |
| | | 430781,津市市,4,430700 |
| | | 430802,永定区,4,430800 |
| | | 430811,武陵源区,4,430800 |
| | | 430821,慈利县,4,430800 |
| | | 430822,桑植县,4,430800 |
| | | 430902,资阳区,4,430900 |
| | | 430903,赫山区,4,430900 |
| | | 430921,南县,4,430900 |
| | | 430922,桃江县,4,430900 |
| | | 430923,安化县,4,430900 |
| | | 430971,益阳市大通湖管理区,4,430900 |
| | | 430972,湖南益阳高新技术产业园区,4,430900 |
| | | 430981,沅江市,4,430900 |
| | | 431002,北湖区,4,431000 |
| | | 431003,苏仙区,4,431000 |
| | | 431021,桂阳县,4,431000 |
| | | 431022,宜章县,4,431000 |
| | | 431023,永兴县,4,431000 |
| | | 431024,嘉禾县,4,431000 |
| | | 431025,临武县,4,431000 |
| | | 431026,汝城县,4,431000 |
| | | 431027,桂东县,4,431000 |
| | | 431028,安仁县,4,431000 |
| | | 431081,资兴市,4,431000 |
| | | 431102,零陵区,4,431100 |
| | | 431103,冷水滩区,4,431100 |
| | | 431122,东安县,4,431100 |
| | | 431123,双牌县,4,431100 |
| | | 431124,道县,4,431100 |
| | | 431125,江永县,4,431100 |
| | | 431126,宁远县,4,431100 |
| | | 431127,蓝山县,4,431100 |
| | | 431128,新田县,4,431100 |
| | | 431129,江华瑶族自治县,4,431100 |
| | | 431171,永州经济技术开发区,4,431100 |
| | | 431173,永州市回龙圩管理区,4,431100 |
| | | 431181,祁阳市,4,431100 |
| | | 431202,鹤城区,4,431200 |
| | | 431221,中方县,4,431200 |
| | | 431222,沅陵县,4,431200 |
| | | 431223,辰溪县,4,431200 |
| | | 431224,溆浦县,4,431200 |
| | | 431225,会同县,4,431200 |
| | | 431226,麻阳苗族自治县,4,431200 |
| | | 431227,新晃侗族自治县,4,431200 |
| | | 431228,芷江侗族自治县,4,431200 |
| | | 431229,靖州苗族侗族自治县,4,431200 |
| | | 431230,通道侗族自治县,4,431200 |
| | | 431271,怀化市洪江管理区,4,431200 |
| | | 431281,洪江市,4,431200 |
| | | 431302,娄星区,4,431300 |
| | | 431321,双峰县,4,431300 |
| | | 431322,新化县,4,431300 |
| | | 431381,冷水江市,4,431300 |
| | | 431382,涟源市,4,431300 |
| | | 433101,吉首市,4,433100 |
| | | 433122,泸溪县,4,433100 |
| | | 433123,凤凰县,4,433100 |
| | | 433124,花垣县,4,433100 |
| | | 433125,保靖县,4,433100 |
| | | 433126,古丈县,4,433100 |
| | | 433127,永顺县,4,433100 |
| | | 433130,龙山县,4,433100 |
| | | 440103,荔湾区,4,440100 |
| | | 440104,越秀区,4,440100 |
| | | 440105,海珠区,4,440100 |
| | | 440106,天河区,4,440100 |
| | | 440111,白云区,4,440100 |
| | | 440112,黄埔区,4,440100 |
| | | 440113,番禺区,4,440100 |
| | | 440114,花都区,4,440100 |
| | | 440115,南沙区,4,440100 |
| | | 440117,从化区,4,440100 |
| | | 440118,增城区,4,440100 |
| | | 440203,武江区,4,440200 |
| | | 440204,浈江区,4,440200 |
| | | 440205,曲江区,4,440200 |
| | | 440222,始兴县,4,440200 |
| | | 440224,仁化县,4,440200 |
| | | 440229,翁源县,4,440200 |
| | | 440232,乳源瑶族自治县,4,440200 |
| | | 440233,新丰县,4,440200 |
| | | 440281,乐昌市,4,440200 |
| | | 440282,南雄市,4,440200 |
| | | 440303,罗湖区,4,440300 |
| | | 440304,福田区,4,440300 |
| | | 440305,南山区,4,440300 |
| | | 440306,宝安区,4,440300 |
| | | 440307,龙岗区,4,440300 |
| | | 440308,盐田区,4,440300 |
| | | 440309,龙华区,4,440300 |
| | | 440310,坪山区,4,440300 |
| | | 440311,光明区,4,440300 |
| | | 440402,香洲区,4,440400 |
| | | 440403,斗门区,4,440400 |
| | | 440404,金湾区,4,440400 |
| | | 440507,龙湖区,4,440500 |
| | | 440511,金平区,4,440500 |
| | | 440512,濠江区,4,440500 |
| | | 440513,潮阳区,4,440500 |
| | | 440514,潮南区,4,440500 |
| | | 440515,澄海区,4,440500 |
| | | 440523,南澳县,4,440500 |
| | | 440604,禅城区,4,440600 |
| | | 440605,南海区,4,440600 |
| | | 440606,顺德区,4,440600 |
| | | 440607,三水区,4,440600 |
| | | 440608,高明区,4,440600 |
| | | 440703,蓬江区,4,440700 |
| | | 440704,江海区,4,440700 |
| | | 440705,新会区,4,440700 |
| | | 440781,台山市,4,440700 |
| | | 440783,开平市,4,440700 |
| | | 440784,鹤山市,4,440700 |
| | | 440785,恩平市,4,440700 |
| | | 440802,赤坎区,4,440800 |
| | | 440803,霞山区,4,440800 |
| | | 440804,坡头区,4,440800 |
| | | 440811,麻章区,4,440800 |
| | | 440823,遂溪县,4,440800 |
| | | 440825,徐闻县,4,440800 |
| | | 440881,廉江市,4,440800 |
| | | 440882,雷州市,4,440800 |
| | | 440883,吴川市,4,440800 |
| | | 440902,茂南区,4,440900 |
| | | 440904,电白区,4,440900 |
| | | 440981,高州市,4,440900 |
| | | 440982,化州市,4,440900 |
| | | 440983,信宜市,4,440900 |
| | | 441202,端州区,4,441200 |
| | | 441203,鼎湖区,4,441200 |
| | | 441204,高要区,4,441200 |
| | | 441223,广宁县,4,441200 |
| | | 441224,怀集县,4,441200 |
| | | 441225,封开县,4,441200 |
| | | 441226,德庆县,4,441200 |
| | | 441284,四会市,4,441200 |
| | | 441302,惠城区,4,441300 |
| | | 441303,惠阳区,4,441300 |
| | | 441322,博罗县,4,441300 |
| | | 441323,惠东县,4,441300 |
| | | 441324,龙门县,4,441300 |
| | | 441402,梅江区,4,441400 |
| | | 441403,梅县区,4,441400 |
| | | 441422,大埔县,4,441400 |
| | | 441423,丰顺县,4,441400 |
| | | 441424,五华县,4,441400 |
| | | 441426,平远县,4,441400 |
| | | 441427,蕉岭县,4,441400 |
| | | 441481,兴宁市,4,441400 |
| | | 441502,城区,4,441500 |
| | | 441521,海丰县,4,441500 |
| | | 441523,陆河县,4,441500 |
| | | 441581,陆丰市,4,441500 |
| | | 441602,源城区,4,441600 |
| | | 441621,紫金县,4,441600 |
| | | 441622,龙川县,4,441600 |
| | | 441623,连平县,4,441600 |
| | | 441624,和平县,4,441600 |
| | | 441625,东源县,4,441600 |
| | | 441702,江城区,4,441700 |
| | | 441704,阳东区,4,441700 |
| | | 441721,阳西县,4,441700 |
| | | 441781,阳春市,4,441700 |
| | | 441802,清城区,4,441800 |
| | | 441803,清新区,4,441800 |
| | | 441821,佛冈县,4,441800 |
| | | 441823,阳山县,4,441800 |
| | | 441825,连山壮族瑶族自治县,4,441800 |
| | | 441826,连南瑶族自治县,4,441800 |
| | | 441881,英德市,4,441800 |
| | | 441882,连州市,4,441800 |
| | | 445102,湘桥区,4,445100 |
| | | 445103,潮安区,4,445100 |
| | | 445122,饶平县,4,445100 |
| | | 445202,榕城区,4,445200 |
| | | 445203,揭东区,4,445200 |
| | | 445222,揭西县,4,445200 |
| | | 445224,惠来县,4,445200 |
| | | 445281,普宁市,4,445200 |
| | | 445302,云城区,4,445300 |
| | | 445303,云安区,4,445300 |
| | | 445321,新兴县,4,445300 |
| | | 445322,郁南县,4,445300 |
| | | 445381,罗定市,4,445300 |
| | | 450102,兴宁区,4,450100 |
| | | 450103,青秀区,4,450100 |
| | | 450105,江南区,4,450100 |
| | | 450107,西乡塘区,4,450100 |
| | | 450108,良庆区,4,450100 |
| | | 450109,邕宁区,4,450100 |
| | | 450110,武鸣区,4,450100 |
| | | 450123,隆安县,4,450100 |
| | | 450124,马山县,4,450100 |
| | | 450125,上林县,4,450100 |
| | | 450126,宾阳县,4,450100 |
| | | 450181,横州市,4,450100 |
| | | 450202,城中区,4,450200 |
| | | 450203,鱼峰区,4,450200 |
| | | 450204,柳南区,4,450200 |
| | | 450205,柳北区,4,450200 |
| | | 450206,柳江区,4,450200 |
| | | 450222,柳城县,4,450200 |
| | | 450223,鹿寨县,4,450200 |
| | | 450224,融安县,4,450200 |
| | | 450225,融水苗族自治县,4,450200 |
| | | 450226,三江侗族自治县,4,450200 |
| | | 450302,秀峰区,4,450300 |
| | | 450303,叠彩区,4,450300 |
| | | 450304,象山区,4,450300 |
| | | 450305,七星区,4,450300 |
| | | 450311,雁山区,4,450300 |
| | | 450312,临桂区,4,450300 |
| | | 450321,阳朔县,4,450300 |
| | | 450323,灵川县,4,450300 |
| | | 450324,全州县,4,450300 |
| | | 450325,兴安县,4,450300 |
| | | 450326,永福县,4,450300 |
| | | 450327,灌阳县,4,450300 |
| | | 450328,龙胜各族自治县,4,450300 |
| | | 450329,资源县,4,450300 |
| | | 450330,平乐县,4,450300 |
| | | 450332,恭城瑶族自治县,4,450300 |
| | | 450381,荔浦市,4,450300 |
| | | 450403,万秀区,4,450400 |
| | | 450405,长洲区,4,450400 |
| | | 450406,龙圩区,4,450400 |
| | | 450421,苍梧县,4,450400 |
| | | 450422,藤县,4,450400 |
| | | 450423,蒙山县,4,450400 |
| | | 450481,岑溪市,4,450400 |
| | | 450502,海城区,4,450500 |
| | | 450503,银海区,4,450500 |
| | | 450512,铁山港区,4,450500 |
| | | 450521,合浦县,4,450500 |
| | | 450602,港口区,4,450600 |
| | | 450603,防城区,4,450600 |
| | | 450621,上思县,4,450600 |
| | | 450681,东兴市,4,450600 |
| | | 450702,钦南区,4,450700 |
| | | 450703,钦北区,4,450700 |
| | | 450721,灵山县,4,450700 |
| | | 450722,浦北县,4,450700 |
| | | 450802,港北区,4,450800 |
| | | 450803,港南区,4,450800 |
| | | 450804,覃塘区,4,450800 |
| | | 450821,平南县,4,450800 |
| | | 450881,桂平市,4,450800 |
| | | 450902,玉州区,4,450900 |
| | | 450903,福绵区,4,450900 |
| | | 450921,容县,4,450900 |
| | | 450922,陆川县,4,450900 |
| | | 450923,博白县,4,450900 |
| | | 450924,兴业县,4,450900 |
| | | 450981,北流市,4,450900 |
| | | 451002,右江区,4,451000 |
| | | 451003,田阳区,4,451000 |
| | | 451022,田东县,4,451000 |
| | | 451024,德保县,4,451000 |
| | | 451026,那坡县,4,451000 |
| | | 451027,凌云县,4,451000 |
| | | 451028,乐业县,4,451000 |
| | | 451029,田林县,4,451000 |
| | | 451030,西林县,4,451000 |
| | | 451031,隆林各族自治县,4,451000 |
| | | 451081,靖西市,4,451000 |
| | | 451082,平果市,4,451000 |
| | | 451102,八步区,4,451100 |
| | | 451103,平桂区,4,451100 |
| | | 451121,昭平县,4,451100 |
| | | 451122,钟山县,4,451100 |
| | | 451123,富川瑶族自治县,4,451100 |
| | | 451202,金城江区,4,451200 |
| | | 451203,宜州区,4,451200 |
| | | 451221,南丹县,4,451200 |
| | | 451222,天峨县,4,451200 |
| | | 451223,凤山县,4,451200 |
| | | 451224,东兰县,4,451200 |
| | | 451225,罗城仫佬族自治县,4,451200 |
| | | 451226,环江毛南族自治县,4,451200 |
| | | 451227,巴马瑶族自治县,4,451200 |
| | | 451228,都安瑶族自治县,4,451200 |
| | | 451229,大化瑶族自治县,4,451200 |
| | | 451302,兴宾区,4,451300 |
| | | 451321,忻城县,4,451300 |
| | | 451322,象州县,4,451300 |
| | | 451323,武宣县,4,451300 |
| | | 451324,金秀瑶族自治县,4,451300 |
| | | 451381,合山市,4,451300 |
| | | 451402,江州区,4,451400 |
| | | 451421,扶绥县,4,451400 |
| | | 451422,宁明县,4,451400 |
| | | 451423,龙州县,4,451400 |
| | | 451424,大新县,4,451400 |
| | | 451425,天等县,4,451400 |
| | | 451481,凭祥市,4,451400 |
| | | 460105,秀英区,4,460100 |
| | | 460106,龙华区,4,460100 |
| | | 460107,琼山区,4,460100 |
| | | 460108,美兰区,4,460100 |
| | | 460202,海棠区,4,460200 |
| | | 460203,吉阳区,4,460200 |
| | | 460204,天涯区,4,460200 |
| | | 460205,崖州区,4,460200 |
| | | 460321,西沙群岛,4,460300 |
| | | 460322,南沙群岛,4,460300 |
| | | 460323,中沙群岛的岛礁及其海域,4,460300 |
| | | 469001,五指山市,4,469000 |
| | | 469002,琼海市,4,469000 |
| | | 469005,文昌市,4,469000 |
| | | 469006,万宁市,4,469000 |
| | | 469007,东方市,4,469000 |
| | | 469021,定安县,4,469000 |
| | | 469022,屯昌县,4,469000 |
| | | 469023,澄迈县,4,469000 |
| | | 469024,临高县,4,469000 |
| | | 469025,白沙黎族自治县,4,469000 |
| | | 469026,昌江黎族自治县,4,469000 |
| | | 469027,乐东黎族自治县,4,469000 |
| | | 469028,陵水黎族自治县,4,469000 |
| | | 469029,保亭黎族苗族自治县,4,469000 |
| | | 469030,琼中黎族苗族自治县,4,469000 |
| | | 500101,万州区,4,500100 |
| | | 500102,涪陵区,4,500100 |
| | | 500103,渝中区,4,500100 |
| | | 500104,大渡口区,4,500100 |
| | | 500105,江北区,4,500100 |
| | | 500106,沙坪坝区,4,500100 |
| | | 500107,九龙坡区,4,500100 |
| | | 500108,南岸区,4,500100 |
| | | 500109,北碚区,4,500100 |
| | | 500110,綦江区,4,500100 |
| | | 500111,大足区,4,500100 |
| | | 500112,渝北区,4,500100 |
| | | 500113,巴南区,4,500100 |
| | | 500114,黔江区,4,500100 |
| | | 500115,长寿区,4,500100 |
| | | 500116,江津区,4,500100 |
| | | 500117,合川区,4,500100 |
| | | 500118,永川区,4,500100 |
| | | 500119,南川区,4,500100 |
| | | 500120,璧山区,4,500100 |
| | | 500151,铜梁区,4,500100 |
| | | 500152,潼南区,4,500100 |
| | | 500153,荣昌区,4,500100 |
| | | 500154,开州区,4,500100 |
| | | 500155,梁平区,4,500100 |
| | | 500156,武隆区,4,500100 |
| | | 500229,城口县,4,500100 |
| | | 500230,丰都县,4,500100 |
| | | 500231,垫江县,4,500100 |
| | | 500233,忠县,4,500100 |
| | | 500235,云阳县,4,500100 |
| | | 500236,奉节县,4,500100 |
| | | 500237,巫山县,4,500100 |
| | | 500238,巫溪县,4,500100 |
| | | 500240,石柱土家族自治县,4,500100 |
| | | 500241,秀山土家族苗族自治县,4,500100 |
| | | 500242,酉阳土家族苗族自治县,4,500100 |
| | | 500243,彭水苗族土家族自治县,4,500100 |
| | | 510104,锦江区,4,510100 |
| | | 510105,青羊区,4,510100 |
| | | 510106,金牛区,4,510100 |
| | | 510107,武侯区,4,510100 |
| | | 510108,成华区,4,510100 |
| | | 510112,龙泉驿区,4,510100 |
| | | 510113,青白江区,4,510100 |
| | | 510114,新都区,4,510100 |
| | | 510115,温江区,4,510100 |
| | | 510116,双流区,4,510100 |
| | | 510117,郫都区,4,510100 |
| | | 510118,新津区,4,510100 |
| | | 510121,金堂县,4,510100 |
| | | 510129,大邑县,4,510100 |
| | | 510131,蒲江县,4,510100 |
| | | 510181,都江堰市,4,510100 |
| | | 510182,彭州市,4,510100 |
| | | 510183,邛崃市,4,510100 |
| | | 510184,崇州市,4,510100 |
| | | 510185,简阳市,4,510100 |
| | | 510302,自流井区,4,510300 |
| | | 510303,贡井区,4,510300 |
| | | 510304,大安区,4,510300 |
| | | 510311,沿滩区,4,510300 |
| | | 510321,荣县,4,510300 |
| | | 510322,富顺县,4,510300 |
| | | 510402,东区,4,510400 |
| | | 510403,西区,4,510400 |
| | | 510411,仁和区,4,510400 |
| | | 510421,米易县,4,510400 |
| | | 510422,盐边县,4,510400 |
| | | 510502,江阳区,4,510500 |
| | | 510503,纳溪区,4,510500 |
| | | 510504,龙马潭区,4,510500 |
| | | 510521,泸县,4,510500 |
| | | 510522,合江县,4,510500 |
| | | 510524,叙永县,4,510500 |
| | | 510525,古蔺县,4,510500 |
| | | 510603,旌阳区,4,510600 |
| | | 510604,罗江区,4,510600 |
| | | 510623,中江县,4,510600 |
| | | 510681,广汉市,4,510600 |
| | | 510682,什邡市,4,510600 |
| | | 510683,绵竹市,4,510600 |
| | | 510703,涪城区,4,510700 |
| | | 510704,游仙区,4,510700 |
| | | 510705,安州区,4,510700 |
| | | 510722,三台县,4,510700 |
| | | 510723,盐亭县,4,510700 |
| | | 510725,梓潼县,4,510700 |
| | | 510726,北川羌族自治县,4,510700 |
| | | 510727,平武县,4,510700 |
| | | 510781,江油市,4,510700 |
| | | 510802,利州区,4,510800 |
| | | 510811,昭化区,4,510800 |
| | | 510812,朝天区,4,510800 |
| | | 510821,旺苍县,4,510800 |
| | | 510822,青川县,4,510800 |
| | | 510823,剑阁县,4,510800 |
| | | 510824,苍溪县,4,510800 |
| | | 510903,船山区,4,510900 |
| | | 510904,安居区,4,510900 |
| | | 510921,蓬溪县,4,510900 |
| | | 510923,大英县,4,510900 |
| | | 510981,射洪市,4,510900 |
| | | 511002,市中区,4,511000 |
| | | 511011,东兴区,4,511000 |
| | | 511024,威远县,4,511000 |
| | | 511025,资中县,4,511000 |
| | | 511071,内江经济开发区,4,511000 |
| | | 511083,隆昌市,4,511000 |
| | | 511102,市中区,4,511100 |
| | | 511111,沙湾区,4,511100 |
| | | 511112,五通桥区,4,511100 |
| | | 511113,金口河区,4,511100 |
| | | 511123,犍为县,4,511100 |
| | | 511124,井研县,4,511100 |
| | | 511126,夹江县,4,511100 |
| | | 511129,沐川县,4,511100 |
| | | 511132,峨边彝族自治县,4,511100 |
| | | 511133,马边彝族自治县,4,511100 |
| | | 511181,峨眉山市,4,511100 |
| | | 511302,顺庆区,4,511300 |
| | | 511303,高坪区,4,511300 |
| | | 511304,嘉陵区,4,511300 |
| | | 511321,南部县,4,511300 |
| | | 511322,营山县,4,511300 |
| | | 511323,蓬安县,4,511300 |
| | | 511324,仪陇县,4,511300 |
| | | 511325,西充县,4,511300 |
| | | 511381,阆中市,4,511300 |
| | | 511402,东坡区,4,511400 |
| | | 511403,彭山区,4,511400 |
| | | 511421,仁寿县,4,511400 |
| | | 511423,洪雅县,4,511400 |
| | | 511424,丹棱县,4,511400 |
| | | 511425,青神县,4,511400 |
| | | 511502,翠屏区,4,511500 |
| | | 511503,南溪区,4,511500 |
| | | 511504,叙州区,4,511500 |
| | | 511523,江安县,4,511500 |
| | | 511524,长宁县,4,511500 |
| | | 511525,高县,4,511500 |
| | | 511526,珙县,4,511500 |
| | | 511527,筠连县,4,511500 |
| | | 511528,兴文县,4,511500 |
| | | 511529,屏山县,4,511500 |
| | | 511602,广安区,4,511600 |
| | | 511603,前锋区,4,511600 |
| | | 511621,岳池县,4,511600 |
| | | 511622,武胜县,4,511600 |
| | | 511623,邻水县,4,511600 |
| | | 511681,华蓥市,4,511600 |
| | | 511702,通川区,4,511700 |
| | | 511703,达川区,4,511700 |
| | | 511722,宣汉县,4,511700 |
| | | 511723,开江县,4,511700 |
| | | 511724,大竹县,4,511700 |
| | | 511725,渠县,4,511700 |
| | | 511771,达州经济开发区,4,511700 |
| | | 511781,万源市,4,511700 |
| | | 511802,雨城区,4,511800 |
| | | 511803,名山区,4,511800 |
| | | 511822,荥经县,4,511800 |
| | | 511823,汉源县,4,511800 |
| | | 511824,石棉县,4,511800 |
| | | 511825,天全县,4,511800 |
| | | 511826,芦山县,4,511800 |
| | | 511827,宝兴县,4,511800 |
| | | 511902,巴州区,4,511900 |
| | | 511903,恩阳区,4,511900 |
| | | 511921,通江县,4,511900 |
| | | 511922,南江县,4,511900 |
| | | 511923,平昌县,4,511900 |
| | | 511971,巴中经济开发区,4,511900 |
| | | 512002,雁江区,4,512000 |
| | | 512021,安岳县,4,512000 |
| | | 512022,乐至县,4,512000 |
| | | 513201,马尔康市,4,513200 |
| | | 513221,汶川县,4,513200 |
| | | 513222,理县,4,513200 |
| | | 513223,茂县,4,513200 |
| | | 513224,松潘县,4,513200 |
| | | 513225,九寨沟县,4,513200 |
| | | 513226,金川县,4,513200 |
| | | 513227,小金县,4,513200 |
| | | 513228,黑水县,4,513200 |
| | | 513230,壤塘县,4,513200 |
| | | 513231,阿坝县,4,513200 |
| | | 513232,若尔盖县,4,513200 |
| | | 513233,红原县,4,513200 |
| | | 513301,康定市,4,513300 |
| | | 513322,泸定县,4,513300 |
| | | 513323,丹巴县,4,513300 |
| | | 513324,九龙县,4,513300 |
| | | 513325,雅江县,4,513300 |
| | | 513326,道孚县,4,513300 |
| | | 513327,炉霍县,4,513300 |
| | | 513328,甘孜县,4,513300 |
| | | 513329,新龙县,4,513300 |
| | | 513330,德格县,4,513300 |
| | | 513331,白玉县,4,513300 |
| | | 513332,石渠县,4,513300 |
| | | 513333,色达县,4,513300 |
| | | 513334,理塘县,4,513300 |
| | | 513335,巴塘县,4,513300 |
| | | 513336,乡城县,4,513300 |
| | | 513337,稻城县,4,513300 |
| | | 513338,得荣县,4,513300 |
| | | 513401,西昌市,4,513400 |
| | | 513402,会理市,4,513400 |
| | | 513422,木里藏族自治县,4,513400 |
| | | 513423,盐源县,4,513400 |
| | | 513424,德昌县,4,513400 |
| | | 513426,会东县,4,513400 |
| | | 513427,宁南县,4,513400 |
| | | 513428,普格县,4,513400 |
| | | 513429,布拖县,4,513400 |
| | | 513430,金阳县,4,513400 |
| | | 513431,昭觉县,4,513400 |
| | | 513432,喜德县,4,513400 |
| | | 513433,冕宁县,4,513400 |
| | | 513434,越西县,4,513400 |
| | | 513435,甘洛县,4,513400 |
| | | 513436,美姑县,4,513400 |
| | | 513437,雷波县,4,513400 |
| | | 520102,南明区,4,520100 |
| | | 520103,云岩区,4,520100 |
| | | 520111,花溪区,4,520100 |
| | | 520112,乌当区,4,520100 |
| | | 520113,白云区,4,520100 |
| | | 520115,观山湖区,4,520100 |
| | | 520121,开阳县,4,520100 |
| | | 520122,息烽县,4,520100 |
| | | 520123,修文县,4,520100 |
| | | 520181,清镇市,4,520100 |
| | | 520201,钟山区,4,520200 |
| | | 520203,六枝特区,4,520200 |
| | | 520204,水城区,4,520200 |
| | | 520281,盘州市,4,520200 |
| | | 520302,红花岗区,4,520300 |
| | | 520303,汇川区,4,520300 |
| | | 520304,播州区,4,520300 |
| | | 520322,桐梓县,4,520300 |
| | | 520323,绥阳县,4,520300 |
| | | 520324,正安县,4,520300 |
| | | 520325,道真仡佬族苗族自治县,4,520300 |
| | | 520326,务川仡佬族苗族自治县,4,520300 |
| | | 520327,凤冈县,4,520300 |
| | | 520328,湄潭县,4,520300 |
| | | 520329,余庆县,4,520300 |
| | | 520330,习水县,4,520300 |
| | | 520381,赤水市,4,520300 |
| | | 520382,仁怀市,4,520300 |
| | | 520402,西秀区,4,520400 |
| | | 520403,平坝区,4,520400 |
| | | 520422,普定县,4,520400 |
| | | 520423,镇宁布依族苗族自治县,4,520400 |
| | | 520424,关岭布依族苗族自治县,4,520400 |
| | | 520425,紫云苗族布依族自治县,4,520400 |
| | | 520502,七星关区,4,520500 |
| | | 520521,大方县,4,520500 |
| | | 520523,金沙县,4,520500 |
| | | 520524,织金县,4,520500 |
| | | 520525,纳雍县,4,520500 |
| | | 520526,威宁彝族回族苗族自治县,4,520500 |
| | | 520527,赫章县,4,520500 |
| | | 520581,黔西市,4,520500 |
| | | 520602,碧江区,4,520600 |
| | | 520603,万山区,4,520600 |
| | | 520621,江口县,4,520600 |
| | | 520622,玉屏侗族自治县,4,520600 |
| | | 520623,石阡县,4,520600 |
| | | 520624,思南县,4,520600 |
| | | 520625,印江土家族苗族自治县,4,520600 |
| | | 520626,德江县,4,520600 |
| | | 520627,沿河土家族自治县,4,520600 |
| | | 520628,松桃苗族自治县,4,520600 |
| | | 522301,兴义市,4,522300 |
| | | 522302,兴仁市,4,522300 |
| | | 522323,普安县,4,522300 |
| | | 522324,晴隆县,4,522300 |
| | | 522325,贞丰县,4,522300 |
| | | 522326,望谟县,4,522300 |
| | | 522327,册亨县,4,522300 |
| | | 522328,安龙县,4,522300 |
| | | 522601,凯里市,4,522600 |
| | | 522622,黄平县,4,522600 |
| | | 522623,施秉县,4,522600 |
| | | 522624,三穗县,4,522600 |
| | | 522625,镇远县,4,522600 |
| | | 522626,岑巩县,4,522600 |
| | | 522627,天柱县,4,522600 |
| | | 522628,锦屏县,4,522600 |
| | | 522629,剑河县,4,522600 |
| | | 522630,台江县,4,522600 |
| | | 522631,黎平县,4,522600 |
| | | 522632,榕江县,4,522600 |
| | | 522633,从江县,4,522600 |
| | | 522634,雷山县,4,522600 |
| | | 522635,麻江县,4,522600 |
| | | 522636,丹寨县,4,522600 |
| | | 522701,都匀市,4,522700 |
| | | 522702,福泉市,4,522700 |
| | | 522722,荔波县,4,522700 |
| | | 522723,贵定县,4,522700 |
| | | 522725,瓮安县,4,522700 |
| | | 522726,独山县,4,522700 |
| | | 522727,平塘县,4,522700 |
| | | 522728,罗甸县,4,522700 |
| | | 522729,长顺县,4,522700 |
| | | 522730,龙里县,4,522700 |
| | | 522731,惠水县,4,522700 |
| | | 522732,三都水族自治县,4,522700 |
| | | 530102,五华区,4,530100 |
| | | 530103,盘龙区,4,530100 |
| | | 530111,官渡区,4,530100 |
| | | 530112,西山区,4,530100 |
| | | 530113,东川区,4,530100 |
| | | 530114,呈贡区,4,530100 |
| | | 530115,晋宁区,4,530100 |
| | | 530124,富民县,4,530100 |
| | | 530125,宜良县,4,530100 |
| | | 530126,石林彝族自治县,4,530100 |
| | | 530127,嵩明县,4,530100 |
| | | 530128,禄劝彝族苗族自治县,4,530100 |
| | | 530129,寻甸回族彝族自治县,4,530100 |
| | | 530181,安宁市,4,530100 |
| | | 530302,麒麟区,4,530300 |
| | | 530303,沾益区,4,530300 |
| | | 530304,马龙区,4,530300 |
| | | 530322,陆良县,4,530300 |
| | | 530323,师宗县,4,530300 |
| | | 530324,罗平县,4,530300 |
| | | 530325,富源县,4,530300 |
| | | 530326,会泽县,4,530300 |
| | | 530381,宣威市,4,530300 |
| | | 530402,红塔区,4,530400 |
| | | 530403,江川区,4,530400 |
| | | 530423,通海县,4,530400 |
| | | 530424,华宁县,4,530400 |
| | | 530425,易门县,4,530400 |
| | | 530426,峨山彝族自治县,4,530400 |
| | | 530427,新平彝族傣族自治县,4,530400 |
| | | 530428,元江哈尼族彝族傣族自治县,4,530400 |
| | | 530481,澄江市,4,530400 |
| | | 530502,隆阳区,4,530500 |
| | | 530521,施甸县,4,530500 |
| | | 530523,龙陵县,4,530500 |
| | | 530524,昌宁县,4,530500 |
| | | 530581,腾冲市,4,530500 |
| | | 530602,昭阳区,4,530600 |
| | | 530621,鲁甸县,4,530600 |
| | | 530622,巧家县,4,530600 |
| | | 530623,盐津县,4,530600 |
| | | 530624,大关县,4,530600 |
| | | 530625,永善县,4,530600 |
| | | 530626,绥江县,4,530600 |
| | | 530627,镇雄县,4,530600 |
| | | 530628,彝良县,4,530600 |
| | | 530629,威信县,4,530600 |
| | | 530681,水富市,4,530600 |
| | | 530702,古城区,4,530700 |
| | | 530721,玉龙纳西族自治县,4,530700 |
| | | 530722,永胜县,4,530700 |
| | | 530723,华坪县,4,530700 |
| | | 530724,宁蒗彝族自治县,4,530700 |
| | | 530802,思茅区,4,530800 |
| | | 530821,宁洱哈尼族彝族自治县,4,530800 |
| | | 530822,墨江哈尼族自治县,4,530800 |
| | | 530823,景东彝族自治县,4,530800 |
| | | 530824,景谷傣族彝族自治县,4,530800 |
| | | 530825,镇沅彝族哈尼族拉祜族自治县,4,530800 |
| | | 530826,江城哈尼族彝族自治县,4,530800 |
| | | 530827,孟连傣族拉祜族佤族自治县,4,530800 |
| | | 530828,澜沧拉祜族自治县,4,530800 |
| | | 530829,西盟佤族自治县,4,530800 |
| | | 530902,临翔区,4,530900 |
| | | 530921,凤庆县,4,530900 |
| | | 530922,云县,4,530900 |
| | | 530923,永德县,4,530900 |
| | | 530924,镇康县,4,530900 |
| | | 530925,双江拉祜族佤族布朗族傣族自治县,4,530900 |
| | | 530926,耿马傣族佤族自治县,4,530900 |
| | | 530927,沧源佤族自治县,4,530900 |
| | | 532301,楚雄市,4,532300 |
| | | 532302,禄丰市,4,532300 |
| | | 532322,双柏县,4,532300 |
| | | 532323,牟定县,4,532300 |
| | | 532324,南华县,4,532300 |
| | | 532325,姚安县,4,532300 |
| | | 532326,大姚县,4,532300 |
| | | 532327,永仁县,4,532300 |
| | | 532328,元谋县,4,532300 |
| | | 532329,武定县,4,532300 |
| | | 532501,个旧市,4,532500 |
| | | 532502,开远市,4,532500 |
| | | 532503,蒙自市,4,532500 |
| | | 532504,弥勒市,4,532500 |
| | | 532523,屏边苗族自治县,4,532500 |
| | | 532524,建水县,4,532500 |
| | | 532525,石屏县,4,532500 |
| | | 532527,泸西县,4,532500 |
| | | 532528,元阳县,4,532500 |
| | | 532529,红河县,4,532500 |
| | | 532530,金平苗族瑶族傣族自治县,4,532500 |
| | | 532531,绿春县,4,532500 |
| | | 532532,河口瑶族自治县,4,532500 |
| | | 532601,文山市,4,532600 |
| | | 532622,砚山县,4,532600 |
| | | 532623,西畴县,4,532600 |
| | | 532624,麻栗坡县,4,532600 |
| | | 532625,马关县,4,532600 |
| | | 532626,丘北县,4,532600 |
| | | 532627,广南县,4,532600 |
| | | 532628,富宁县,4,532600 |
| | | 532801,景洪市,4,532800 |
| | | 532822,勐海县,4,532800 |
| | | 532823,勐腊县,4,532800 |
| | | 532901,大理市,4,532900 |
| | | 532922,漾濞彝族自治县,4,532900 |
| | | 532923,祥云县,4,532900 |
| | | 532924,宾川县,4,532900 |
| | | 532925,弥渡县,4,532900 |
| | | 532926,南涧彝族自治县,4,532900 |
| | | 532927,巍山彝族回族自治县,4,532900 |
| | | 532928,永平县,4,532900 |
| | | 532929,云龙县,4,532900 |
| | | 532930,洱源县,4,532900 |
| | | 532931,剑川县,4,532900 |
| | | 532932,鹤庆县,4,532900 |
| | | 533102,瑞丽市,4,533100 |
| | | 533103,芒市,4,533100 |
| | | 533122,梁河县,4,533100 |
| | | 533123,盈江县,4,533100 |
| | | 533124,陇川县,4,533100 |
| | | 533301,泸水市,4,533300 |
| | | 533323,福贡县,4,533300 |
| | | 533324,贡山独龙族怒族自治县,4,533300 |
| | | 533325,兰坪白族普米族自治县,4,533300 |
| | | 533401,香格里拉市,4,533400 |
| | | 533422,德钦县,4,533400 |
| | | 533423,维西傈僳族自治县,4,533400 |
| | | 540102,城关区,4,540100 |
| | | 540103,堆龙德庆区,4,540100 |
| | | 540104,达孜区,4,540100 |
| | | 540121,林周县,4,540100 |
| | | 540122,当雄县,4,540100 |
| | | 540123,尼木县,4,540100 |
| | | 540124,曲水县,4,540100 |
| | | 540127,墨竹工卡县,4,540100 |
| | | 540171,格尔木藏青工业园区,4,540100 |
| | | 540172,拉萨经济技术开发区,4,540100 |
| | | 540173,西藏文化旅游创意园区,4,540100 |
| | | 540174,达孜工业园区,4,540100 |
| | | 540202,桑珠孜区,4,540200 |
| | | 540221,南木林县,4,540200 |
| | | 540222,江孜县,4,540200 |
| | | 540223,定日县,4,540200 |
| | | 540224,萨迦县,4,540200 |
| | | 540225,拉孜县,4,540200 |
| | | 540226,昂仁县,4,540200 |
| | | 540227,谢通门县,4,540200 |
| | | 540228,白朗县,4,540200 |
| | | 540229,仁布县,4,540200 |
| | | 540230,康马县,4,540200 |
| | | 540231,定结县,4,540200 |
| | | 540232,仲巴县,4,540200 |
| | | 540233,亚东县,4,540200 |
| | | 540234,吉隆县,4,540200 |
| | | 540235,聂拉木县,4,540200 |
| | | 540236,萨嘎县,4,540200 |
| | | 540237,岗巴县,4,540200 |
| | | 540302,卡若区,4,540300 |
| | | 540321,江达县,4,540300 |
| | | 540322,贡觉县,4,540300 |
| | | 540323,类乌齐县,4,540300 |
| | | 540324,丁青县,4,540300 |
| | | 540325,察雅县,4,540300 |
| | | 540326,八宿县,4,540300 |
| | | 540327,左贡县,4,540300 |
| | | 540328,芒康县,4,540300 |
| | | 540329,洛隆县,4,540300 |
| | | 540330,边坝县,4,540300 |
| | | 540402,巴宜区,4,540400 |
| | | 540421,工布江达县,4,540400 |
| | | 540422,米林县,4,540400 |
| | | 540423,墨脱县,4,540400 |
| | | 540424,波密县,4,540400 |
| | | 540425,察隅县,4,540400 |
| | | 540426,朗县,4,540400 |
| | | 540502,乃东区,4,540500 |
| | | 540521,扎囊县,4,540500 |
| | | 540522,贡嘎县,4,540500 |
| | | 540523,桑日县,4,540500 |
| | | 540524,琼结县,4,540500 |
| | | 540525,曲松县,4,540500 |
| | | 540526,措美县,4,540500 |
| | | 540527,洛扎县,4,540500 |
| | | 540528,加查县,4,540500 |
| | | 540529,隆子县,4,540500 |
| | | 540530,错那县,4,540500 |
| | | 540531,浪卡子县,4,540500 |
| | | 540602,色尼区,4,540600 |
| | | 540621,嘉黎县,4,540600 |
| | | 540622,比如县,4,540600 |
| | | 540623,聂荣县,4,540600 |
| | | 540624,安多县,4,540600 |
| | | 540625,申扎县,4,540600 |
| | | 540626,索县,4,540600 |
| | | 540627,班戈县,4,540600 |
| | | 540628,巴青县,4,540600 |
| | | 540629,尼玛县,4,540600 |
| | | 540630,双湖县,4,540600 |
| | | 542521,普兰县,4,542500 |
| | | 542522,札达县,4,542500 |
| | | 542523,噶尔县,4,542500 |
| | | 542524,日土县,4,542500 |
| | | 542525,革吉县,4,542500 |
| | | 542526,改则县,4,542500 |
| | | 542527,措勤县,4,542500 |
| | | 610102,新城区,4,610100 |
| | | 610103,碑林区,4,610100 |
| | | 610104,莲湖区,4,610100 |
| | | 610111,灞桥区,4,610100 |
| | | 610112,未央区,4,610100 |
| | | 610113,雁塔区,4,610100 |
| | | 610114,阎良区,4,610100 |
| | | 610115,临潼区,4,610100 |
| | | 610116,长安区,4,610100 |
| | | 610117,高陵区,4,610100 |
| | | 610118,鄠邑区,4,610100 |
| | | 610122,蓝田县,4,610100 |
| | | 610124,周至县,4,610100 |
| | | 610202,王益区,4,610200 |
| | | 610203,印台区,4,610200 |
| | | 610204,耀州区,4,610200 |
| | | 610222,宜君县,4,610200 |
| | | 610302,渭滨区,4,610300 |
| | | 610303,金台区,4,610300 |
| | | 610304,陈仓区,4,610300 |
| | | 610305,凤翔区,4,610300 |
| | | 610323,岐山县,4,610300 |
| | | 610324,扶风县,4,610300 |
| | | 610326,眉县,4,610300 |
| | | 610327,陇县,4,610300 |
| | | 610328,千阳县,4,610300 |
| | | 610329,麟游县,4,610300 |
| | | 610330,凤县,4,610300 |
| | | 610331,太白县,4,610300 |
| | | 610402,秦都区,4,610400 |
| | | 610403,杨陵区,4,610400 |
| | | 610404,渭城区,4,610400 |
| | | 610422,三原县,4,610400 |
| | | 610423,泾阳县,4,610400 |
| | | 610424,乾县,4,610400 |
| | | 610425,礼泉县,4,610400 |
| | | 610426,永寿县,4,610400 |
| | | 610428,长武县,4,610400 |
| | | 610429,旬邑县,4,610400 |
| | | 610430,淳化县,4,610400 |
| | | 610431,武功县,4,610400 |
| | | 610481,兴平市,4,610400 |
| | | 610482,彬州市,4,610400 |
| | | 610502,临渭区,4,610500 |
| | | 610503,华州区,4,610500 |
| | | 610522,潼关县,4,610500 |
| | | 610523,大荔县,4,610500 |
| | | 610524,合阳县,4,610500 |
| | | 610525,澄城县,4,610500 |
| | | 610526,蒲城县,4,610500 |
| | | 610527,白水县,4,610500 |
| | | 610528,富平县,4,610500 |
| | | 610581,韩城市,4,610500 |
| | | 610582,华阴市,4,610500 |
| | | 610602,宝塔区,4,610600 |
| | | 610603,安塞区,4,610600 |
| | | 610621,延长县,4,610600 |
| | | 610622,延川县,4,610600 |
| | | 610625,志丹县,4,610600 |
| | | 610626,吴起县,4,610600 |
| | | 610627,甘泉县,4,610600 |
| | | 610628,富县,4,610600 |
| | | 610629,洛川县,4,610600 |
| | | 610630,宜川县,4,610600 |
| | | 610631,黄龙县,4,610600 |
| | | 610632,黄陵县,4,610600 |
| | | 610681,子长市,4,610600 |
| | | 610702,汉台区,4,610700 |
| | | 610703,南郑区,4,610700 |
| | | 610722,城固县,4,610700 |
| | | 610723,洋县,4,610700 |
| | | 610724,西乡县,4,610700 |
| | | 610725,勉县,4,610700 |
| | | 610726,宁强县,4,610700 |
| | | 610727,略阳县,4,610700 |
| | | 610728,镇巴县,4,610700 |
| | | 610729,留坝县,4,610700 |
| | | 610730,佛坪县,4,610700 |
| | | 610802,榆阳区,4,610800 |
| | | 610803,横山区,4,610800 |
| | | 610822,府谷县,4,610800 |
| | | 610824,靖边县,4,610800 |
| | | 610825,定边县,4,610800 |
| | | 610826,绥德县,4,610800 |
| | | 610827,米脂县,4,610800 |
| | | 610828,佳县,4,610800 |
| | | 610829,吴堡县,4,610800 |
| | | 610830,清涧县,4,610800 |
| | | 610831,子洲县,4,610800 |
| | | 610881,神木市,4,610800 |
| | | 610902,汉滨区,4,610900 |
| | | 610921,汉阴县,4,610900 |
| | | 610922,石泉县,4,610900 |
| | | 610923,宁陕县,4,610900 |
| | | 610924,紫阳县,4,610900 |
| | | 610925,岚皋县,4,610900 |
| | | 610926,平利县,4,610900 |
| | | 610927,镇坪县,4,610900 |
| | | 610929,白河县,4,610900 |
| | | 610981,旬阳市,4,610900 |
| | | 611002,商州区,4,611000 |
| | | 611021,洛南县,4,611000 |
| | | 611022,丹凤县,4,611000 |
| | | 611023,商南县,4,611000 |
| | | 611024,山阳县,4,611000 |
| | | 611025,镇安县,4,611000 |
| | | 611026,柞水县,4,611000 |
| | | 620102,城关区,4,620100 |
| | | 620103,七里河区,4,620100 |
| | | 620104,西固区,4,620100 |
| | | 620105,安宁区,4,620100 |
| | | 620111,红古区,4,620100 |
| | | 620121,永登县,4,620100 |
| | | 620122,皋兰县,4,620100 |
| | | 620123,榆中县,4,620100 |
| | | 620171,兰州新区,4,620100 |
| | | 620201,嘉峪关市,4,620200 |
| | | 620302,金川区,4,620300 |
| | | 620321,永昌县,4,620300 |
| | | 620402,白银区,4,620400 |
| | | 620403,平川区,4,620400 |
| | | 620421,靖远县,4,620400 |
| | | 620422,会宁县,4,620400 |
| | | 620423,景泰县,4,620400 |
| | | 620502,秦州区,4,620500 |
| | | 620503,麦积区,4,620500 |
| | | 620521,清水县,4,620500 |
| | | 620522,秦安县,4,620500 |
| | | 620523,甘谷县,4,620500 |
| | | 620524,武山县,4,620500 |
| | | 620525,张家川回族自治县,4,620500 |
| | | 620602,凉州区,4,620600 |
| | | 620621,民勤县,4,620600 |
| | | 620622,古浪县,4,620600 |
| | | 620623,天祝藏族自治县,4,620600 |
| | | 620702,甘州区,4,620700 |
| | | 620721,肃南裕固族自治县,4,620700 |
| | | 620722,民乐县,4,620700 |
| | | 620723,临泽县,4,620700 |
| | | 620724,高台县,4,620700 |
| | | 620725,山丹县,4,620700 |
| | | 620802,崆峒区,4,620800 |
| | | 620821,泾川县,4,620800 |
| | | 620822,灵台县,4,620800 |
| | | 620823,崇信县,4,620800 |
| | | 620825,庄浪县,4,620800 |
| | | 620826,静宁县,4,620800 |
| | | 620881,华亭市,4,620800 |
| | | 620902,肃州区,4,620900 |
| | | 620921,金塔县,4,620900 |
| | | 620922,瓜州县,4,620900 |
| | | 620923,肃北蒙古族自治县,4,620900 |
| | | 620924,阿克塞哈萨克族自治县,4,620900 |
| | | 620981,玉门市,4,620900 |
| | | 620982,敦煌市,4,620900 |
| | | 621002,西峰区,4,621000 |
| | | 621021,庆城县,4,621000 |
| | | 621022,环县,4,621000 |
| | | 621023,华池县,4,621000 |
| | | 621024,合水县,4,621000 |
| | | 621025,正宁县,4,621000 |
| | | 621026,宁县,4,621000 |
| | | 621027,镇原县,4,621000 |
| | | 621102,安定区,4,621100 |
| | | 621121,通渭县,4,621100 |
| | | 621122,陇西县,4,621100 |
| | | 621123,渭源县,4,621100 |
| | | 621124,临洮县,4,621100 |
| | | 621125,漳县,4,621100 |
| | | 621126,岷县,4,621100 |
| | | 621202,武都区,4,621200 |
| | | 621221,成县,4,621200 |
| | | 621222,文县,4,621200 |
| | | 621223,宕昌县,4,621200 |
| | | 621224,康县,4,621200 |
| | | 621225,西和县,4,621200 |
| | | 621226,礼县,4,621200 |
| | | 621227,徽县,4,621200 |
| | | 621228,两当县,4,621200 |
| | | 622901,临夏市,4,622900 |
| | | 622921,临夏县,4,622900 |
| | | 622922,康乐县,4,622900 |
| | | 622923,永靖县,4,622900 |
| | | 622924,广河县,4,622900 |
| | | 622925,和政县,4,622900 |
| | | 622926,东乡族自治县,4,622900 |
| | | 622927,积石山保安族东乡族撒拉族自治县,4,622900 |
| | | 623001,合作市,4,623000 |
| | | 623021,临潭县,4,623000 |
| | | 623022,卓尼县,4,623000 |
| | | 623023,舟曲县,4,623000 |
| | | 623024,迭部县,4,623000 |
| | | 623025,玛曲县,4,623000 |
| | | 623026,碌曲县,4,623000 |
| | | 623027,夏河县,4,623000 |
| | | 630102,城东区,4,630100 |
| | | 630103,城中区,4,630100 |
| | | 630104,城西区,4,630100 |
| | | 630105,城北区,4,630100 |
| | | 630106,湟中区,4,630100 |
| | | 630121,大通回族土族自治县,4,630100 |
| | | 630123,湟源县,4,630100 |
| | | 630202,乐都区,4,630200 |
| | | 630203,平安区,4,630200 |
| | | 630222,民和回族土族自治县,4,630200 |
| | | 630223,互助土族自治县,4,630200 |
| | | 630224,化隆回族自治县,4,630200 |
| | | 630225,循化撒拉族自治县,4,630200 |
| | | 632221,门源回族自治县,4,632200 |
| | | 632222,祁连县,4,632200 |
| | | 632223,海晏县,4,632200 |
| | | 632224,刚察县,4,632200 |
| | | 632301,同仁市,4,632300 |
| | | 632322,尖扎县,4,632300 |
| | | 632323,泽库县,4,632300 |
| | | 632324,河南蒙古族自治县,4,632300 |
| | | 632521,共和县,4,632500 |
| | | 632522,同德县,4,632500 |
| | | 632523,贵德县,4,632500 |
| | | 632524,兴海县,4,632500 |
| | | 632525,贵南县,4,632500 |
| | | 632621,玛沁县,4,632600 |
| | | 632622,班玛县,4,632600 |
| | | 632623,甘德县,4,632600 |
| | | 632624,达日县,4,632600 |
| | | 632625,久治县,4,632600 |
| | | 632626,玛多县,4,632600 |
| | | 632701,玉树市,4,632700 |
| | | 632722,杂多县,4,632700 |
| | | 632723,称多县,4,632700 |
| | | 632724,治多县,4,632700 |
| | | 632725,囊谦县,4,632700 |
| | | 632726,曲麻莱县,4,632700 |
| | | 632801,格尔木市,4,632800 |
| | | 632802,德令哈市,4,632800 |
| | | 632803,茫崖市,4,632800 |
| | | 632821,乌兰县,4,632800 |
| | | 632822,都兰县,4,632800 |
| | | 632823,天峻县,4,632800 |
| | | 632857,大柴旦行政委员会,4,632800 |
| | | 640104,兴庆区,4,640100 |
| | | 640105,西夏区,4,640100 |
| | | 640106,金凤区,4,640100 |
| | | 640121,永宁县,4,640100 |
| | | 640122,贺兰县,4,640100 |
| | | 640181,灵武市,4,640100 |
| | | 640202,大武口区,4,640200 |
| | | 640205,惠农区,4,640200 |
| | | 640221,平罗县,4,640200 |
| | | 640302,利通区,4,640300 |
| | | 640303,红寺堡区,4,640300 |
| | | 640323,盐池县,4,640300 |
| | | 640324,同心县,4,640300 |
| | | 640381,青铜峡市,4,640300 |
| | | 640402,原州区,4,640400 |
| | | 640422,西吉县,4,640400 |
| | | 640423,隆德县,4,640400 |
| | | 640424,泾源县,4,640400 |
| | | 640425,彭阳县,4,640400 |
| | | 640502,沙坡头区,4,640500 |
| | | 640521,中宁县,4,640500 |
| | | 640522,海原县,4,640500 |
| | | 650102,天山区,4,650100 |
| | | 650103,沙依巴克区,4,650100 |
| | | 650104,新市区,4,650100 |
| | | 650105,水磨沟区,4,650100 |
| | | 650106,头屯河区,4,650100 |
| | | 650107,达坂城区,4,650100 |
| | | 650109,米东区,4,650100 |
| | | 650121,乌鲁木齐县,4,650100 |
| | | 650202,独山子区,4,650200 |
| | | 650203,克拉玛依区,4,650200 |
| | | 650204,白碱滩区,4,650200 |
| | | 650205,乌尔禾区,4,650200 |
| | | 650402,高昌区,4,650400 |
| | | 650421,鄯善县,4,650400 |
| | | 650422,托克逊县,4,650400 |
| | | 650502,伊州区,4,650500 |
| | | 650521,巴里坤哈萨克自治县,4,650500 |
| | | 650522,伊吾县,4,650500 |
| | | 652301,昌吉市,4,652300 |
| | | 652302,阜康市,4,652300 |
| | | 652323,呼图壁县,4,652300 |
| | | 652324,玛纳斯县,4,652300 |
| | | 652325,奇台县,4,652300 |
| | | 652327,吉木萨尔县,4,652300 |
| | | 652328,木垒哈萨克自治县,4,652300 |
| | | 652701,博乐市,4,652700 |
| | | 652702,阿拉山口市,4,652700 |
| | | 652722,精河县,4,652700 |
| | | 652723,温泉县,4,652700 |
| | | 652801,库尔勒市,4,652800 |
| | | 652822,轮台县,4,652800 |
| | | 652823,尉犁县,4,652800 |
| | | 652824,若羌县,4,652800 |
| | | 652825,且末县,4,652800 |
| | | 652826,焉耆回族自治县,4,652800 |
| | | 652827,和静县,4,652800 |
| | | 652828,和硕县,4,652800 |
| | | 652829,博湖县,4,652800 |
| | | 652871,库尔勒经济技术开发区,4,652800 |
| | | 652901,阿克苏市,4,652900 |
| | | 652902,库车市,4,652900 |
| | | 652922,温宿县,4,652900 |
| | | 652924,沙雅县,4,652900 |
| | | 652925,新和县,4,652900 |
| | | 652926,拜城县,4,652900 |
| | | 652927,乌什县,4,652900 |
| | | 652928,阿瓦提县,4,652900 |
| | | 652929,柯坪县,4,652900 |
| | | 653001,阿图什市,4,653000 |
| | | 653022,阿克陶县,4,653000 |
| | | 653023,阿合奇县,4,653000 |
| | | 653024,乌恰县,4,653000 |
| | | 653101,喀什市,4,653100 |
| | | 653121,疏附县,4,653100 |
| | | 653122,疏勒县,4,653100 |
| | | 653123,英吉沙县,4,653100 |
| | | 653124,泽普县,4,653100 |
| | | 653125,莎车县,4,653100 |
| | | 653126,叶城县,4,653100 |
| | | 653127,麦盖提县,4,653100 |
| | | 653128,岳普湖县,4,653100 |
| | | 653129,伽师县,4,653100 |
| | | 653130,巴楚县,4,653100 |
| | | 653131,塔什库尔干塔吉克自治县,4,653100 |
| | | 653201,和田市,4,653200 |
| | | 653221,和田县,4,653200 |
| | | 653222,墨玉县,4,653200 |
| | | 653223,皮山县,4,653200 |
| | | 653224,洛浦县,4,653200 |
| | | 653225,策勒县,4,653200 |
| | | 653226,于田县,4,653200 |
| | | 653227,民丰县,4,653200 |
| | | 654002,伊宁市,4,654000 |
| | | 654003,奎屯市,4,654000 |
| | | 654004,霍尔果斯市,4,654000 |
| | | 654021,伊宁县,4,654000 |
| | | 654022,察布查尔锡伯自治县,4,654000 |
| | | 654023,霍城县,4,654000 |
| | | 654024,巩留县,4,654000 |
| | | 654025,新源县,4,654000 |
| | | 654026,昭苏县,4,654000 |
| | | 654027,特克斯县,4,654000 |
| | | 654028,尼勒克县,4,654000 |
| | | 654201,塔城市,4,654200 |
| | | 654202,乌苏市,4,654200 |
| | | 654203,沙湾市,4,654200 |
| | | 654221,额敏县,4,654200 |
| | | 654224,托里县,4,654200 |
| | | 654225,裕民县,4,654200 |
| | | 654226,和布克赛尔蒙古自治县,4,654200 |
| | | 654301,阿勒泰市,4,654300 |
| | | 654321,布尔津县,4,654300 |
| | | 654322,富蕴县,4,654300 |
| | | 654323,福海县,4,654300 |
| | | 654324,哈巴河县,4,654300 |
| | | 654325,青河县,4,654300 |
| | | 654326,吉木乃县,4,654300 |
| | | 659001,石河子市,4,659000 |
| | | 659002,阿拉尔市,4,659000 |
| | | 659003,图木舒克市,4,659000 |
| | | 659004,五家渠市,4,659000 |
| | | 659005,北屯市,4,659000 |
| | | 659006,铁门关市,4,659000 |
| | | 659007,双河市,4,659000 |
| | | 659008,可克达拉市,4,659000 |
| | | 659009,昆玉市,4,659000 |
| | | 659010,胡杨河市,4,659000 |
| | | 659011,新星市,4,659000 |
对比新文件 |
| | |
| | | package com.iailab.framework.ip.core.utils; |
| | | |
| | | |
| | | import com.iailab.framework.ip.core.Area; |
| | | import com.iailab.framework.ip.core.enums.AreaTypeEnum; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | |
| | | /** |
| | | * {@link AreaUtils} 的单元测试 |
| | | * |
| | | * @author iailab |
| | | */ |
| | | public class AreaUtilsTest { |
| | | |
| | | @Test |
| | | public void testGetArea() { |
| | | // 调用:北京 |
| | | Area area = AreaUtils.getArea(110100); |
| | | // 断言 |
| | | assertEquals(area.getId(), 110100); |
| | | assertEquals(area.getName(), "北京市"); |
| | | assertEquals(area.getType(), AreaTypeEnum.CITY.getType()); |
| | | assertEquals(area.getParent().getId(), 110000); |
| | | assertEquals(area.getChildren().size(), 16); |
| | | } |
| | | |
| | | @Test |
| | | public void testFormat() { |
| | | assertEquals(AreaUtils.format(110105), "北京 北京市 朝阳区"); |
| | | assertEquals(AreaUtils.format(1), "中国"); |
| | | assertEquals(AreaUtils.format(2), "蒙古"); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.iailab.framework.ip.core.utils; |
| | | |
| | | import com.iailab.framework.ip.core.Area; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.lionsoul.ip2region.xdb.Searcher; |
| | | |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | |
| | | /** |
| | | * {@link IPUtils} 的单元测试 |
| | | * |
| | | * @author wanglhup |
| | | */ |
| | | public class IPUtilsTest { |
| | | |
| | | @Test |
| | | public void testGetAreaId_string() { |
| | | // 120.202.4.0|120.202.4.255|420600 |
| | | Integer areaId = IPUtils.getAreaId("120.202.4.50"); |
| | | assertEquals(420600, areaId); |
| | | } |
| | | |
| | | @Test |
| | | public void testGetAreaId_long() throws Exception { |
| | | // 120.203.123.0|120.203.133.255|360900 |
| | | long ip = Searcher.checkIP("120.203.123.250"); |
| | | Integer areaId = IPUtils.getAreaId(ip); |
| | | assertEquals(360900, areaId); |
| | | } |
| | | |
| | | @Test |
| | | public void testGetArea_string() { |
| | | // 120.202.4.0|120.202.4.255|420600 |
| | | Area area = IPUtils.getArea("120.202.4.50"); |
| | | assertEquals("襄阳市", area.getName()); |
| | | } |
| | | |
| | | @Test |
| | | public void testGetArea_long() throws Exception { |
| | | // 120.203.123.0|120.203.133.255|360900 |
| | | long ip = Searcher.checkIP("120.203.123.252"); |
| | | Area area = IPUtils.getArea(ip); |
| | | assertEquals("宜春市", area.getName()); |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | <?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)); |
| | | } |
| | | |
| | | } |
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalExceptionHandler.java
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/handler/GlobalResponseBodyHandler.java
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/core/util/WebFrameworkUtils.java
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/web/package-info.java
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/IailabXssAutoConfiguration.java
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/config/XssProperties.java
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/JsoupXssCleaner.java
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/clean/XssCleaner.java
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssFilter.java
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/filter/XssRequestWrapper.java
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/core/json/XssStringJsonDeserializer.java
iailab-framework/iailab-common-web/src/main/java/com/iailab/framework/xss/package-info.java
iailab-framework/iailab-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
iailab-framework/iailab-common-web/src/main/resources/banner.txt
iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/DesensitizeTest.java
iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/annotation/Address.java
iailab-framework/iailab-common-web/src/test/java/com/iailab/framework/desensitize/core/handler/AddressHandler.java
iailab-framework/iailab-common-websocket/pom.xml
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/IailabWebSocketAutoConfiguration.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/config/WebSocketProperties.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/listener/WebSocketMessageListener.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/message/JsonWebSocketMessage.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/LoginUserHandshakeInterceptor.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/AbstractWebSocketMessageSender.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/WebSocketMessageSender.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessage.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManager.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/session/WebSocketSessionManagerImpl.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/core/util/WebSocketFrameworkUtils.java
iailab-framework/iailab-common-websocket/src/main/java/com/iailab/framework/websocket/package-info.java
iailab-framework/iailab-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
iailab-framework/iailab-common/pom.xml
iailab-framework/iailab-common/src/main/java/com/fhs/trans/service/AutoTransable.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoBpm.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoDict.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/AutoUser.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmProcess.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/BpmStatus.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/Dict.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/annotation/UserRealName.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CacheConstant.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/CommonConstant.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/Constant.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/constant/GlobalConstants.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/IntArrayValuable.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/core/KeyValue.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/dto/TreeLabelDTO.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/CommonStatusEnum.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DateIntervalEnum.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/DocumentEnum.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/ErrorCode.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/RpcConstants.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/TerminalEnum.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/UserTypeEnum.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/enums/WebFilterOrderEnum.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ErrorCode.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ExceptionUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServerException.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/ServiceException.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/GlobalErrorCodeConstants.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/enums/ServiceErrorCodeRange.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/exception/util/ServiceExceptionUtil.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/package-info.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/CommonResult.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageParam.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/PageResult.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortablePageParam.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/pojo/SortingField.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/cache/CacheUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/ArrayUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/CollectionUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/MapUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/collection/SetUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/DateUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/date/LocalDateTimeUtils.java
iailab-framework/iailab-common/src/main/java/com/iailab/framework/common/util/http/HttpUtils.java
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
pom.xml |