利用Mybatis拦截器实现自定义的ID增长器

介绍

  原生的Mybatis框架是没有ID自增器,但例如国产的Mybatis Plus却是支持,不过,Mybatis Plus却是缺少了自定属性的填充;例如:我们需要自定义填充一些属性,updateDate、createDate等,这时Mybatis Plus自带的ID自增器就无法满足需求;这种时候我们就需要自定义的ID增加器,可以自定义ID增长策略同时还得支持更多的属性自定义扩展,当然,最好能做成插件形式为其他项目或者模块提供引入那就更新好了。

原理图

在开始之前我们先确定和分析主要的需求,了解了需求才能更好的制作

  首先我们得确定主要的宗旨那就是实现ID自增长器,同时,还等保证该增长的灵活性和扩展性,让其他项目引入之后可以很灵活的更改增长策略,随意替换ID增长的实现。主要需求为下列几点:

  1. 自定义的ID增长策略,同时保证该特性灵活性可以随意替换
  2. 支持自定义的附带属性扩展增强
  3. 保证其他的项目引起使用时的绝对简单,最好能像Spring Boot自动装配模块一样保证简单

  确定需求之后我们现在开始根据需求来功能了,我们由外到内、由粗到细的去实现整个模块。

  先确定该模块的外体特征,先看第3点

保证其他的项目引起使用时的绝对简单,最好能像Spring Boot自动装配模块一样保证简单

创建模块

        要保证该模块的引用使用简单那么就必须使用Spring Boot的特性->自动配置,实现自定义场景装配器,利用该场景装配才能保证模块可以自动配置完成启动之初的初始化。

我们先来新建Maven模块

        这里的模块命名最好遵循Spring Boot官方的建议,第三方的模块命名由模块名称+spring-boot;官方模块由spring-boot+模块名称

建好模块之后我们来整理下目录

这里我们把多余的目录、文件删除,这里使用了Maven的初始化模板,会自动生成一些模板文件;但是,该操作的主要目的只是获得一个Maven结构的项目

接下来确定POM文件的引用

<!-- springboot自动配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>

        <!-- springboot自动配置处理器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- mybatis启动器,本质是在Mybatis的基础上进行扩展的,必须引入Mybatis的支持-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

到这里整个模块的创建已经完成了,接下来我们的仔细分析下该如何实现自定义的增长器

 插件要使用方便那么就意味着要抛弃繁杂的配置,同时对必要的信息注入配置时应该采用注解的方式来保持简洁;

既然是ID增长器那就必须的确定哪个属性为ID,并且确定ID属性的类的全限定名,在这里我们定义两个注解

1、这里定义了@Key注解,该注解的主要目的是为了标识出ORM实体类映射数据库表中的主键,插件最终的主要注入的属性

/**
 * 主键注解标记
 * @Author: Song L.Lu
 * @Since: 2024-01-18 11:20
 **/

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Key {
    String value() default "";
}

2、@KeyGenerator 注解,用来标记当前的实体类所在的包路径,该路径为插件提供查找实体类的路径;注意该注解标记于Spring Boot启动主类上

格式@KeyGenerator ("xxx.xx.xx.entity")

/**
 * 标记当前实体类所在的包路径
 * @Author: Song L.Lu
 * @Since: 2024-01-18 11:19
 **/

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface KeyGenerator {
    String value();
}

3、创建一个Spring Boot 的AutoConfiguration用来自动完成插件的装配,包括初始化上下文环境、扫描Entity实体类、注入Mybatis拦截器

GeneratorAutoConfiguration类由MATE-INFO/spring。factories注入,该配置文件在Spring Boot启动时会自动扫描各模块下的resource/MATE-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.luxsong.starter.mybatis.generator.config.autoconfigure.GeneratorAutoConfiguration

这里往Spring Context中注入了两个实例对象

ParseMapperFactoryProcessor实现BeanFactoryPostProcessor.postProcessBeanFactory接口方法,其目的是在工厂创建并扫描完Beandefition后触发实体类的扫描
GeneratorInterceptor实现了Mybatis的拦截器,其拦截活动为update/insert,在Mybatis触发update/insert操作时进行ID增长和自定义的属性添加效果
/**
 * 自动装配
 * @Author: Song L.Lu
 * @Since: 2024-01-18 11:19
 **/

@ConditionalOnBean({MybatisAutoConfiguration.class})
@AutoConfigureAfter({MybatisAutoConfiguration.class, SnowflakeIdGenerator.class})
public class GeneratorAutoConfiguration {

    /**
     * 注入实体类扫描处理器
     * 主要在BeanFactoryPostProcessor.postProcessBeanFactory期间完成实体类的扫描
     * @return
     */
    @Bean
    public BeanDefinitionRegistryPostProcessor postProcessor() {
        return new ParseMapperFactoryProcessor();
    }

    /**
     * 向Spring context注入Mybatis拦截器
     * @param generator
     * @param mapperRegisterStore
     * @return
     */
    @Bean
    public Interceptor interceptor(Generator<?> generator, MapperRegisterStore mapperRegisterStore) {
        return new GeneratorInterceptor(generator, mapperRegisterStore);
    }
}

4、ParseMapperFactoryProcessor类

/**
 * 实体类扫描处理器
 * 主要工作完成@Key注解的扫描,确定标记的主键
 * @Author: Song L.Lu
 * @Since: 2024-01-18 11:17
 **/
public class ParseMapperFactoryProcessorimplements BeanDefinitionRegistryPostProcessor {
    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    private final MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory();

    private final String resourcePattern = "**/*.class";

    private Environment environment;

    private ResourcePatternResolver resourcePatternResolver;

    public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
        AnnotationMetadata metadata;
        String className = beanDef.getBeanClassName();
        if (className == null || beanDef.getFactoryMethodName() != null)
            return false;
        if (beanDef instanceof AnnotatedBeanDefinition && className
                .equals(((AnnotatedBeanDefinition)beanDef).getMetadata().getClassName())) {
            metadata = ((AnnotatedBeanDefinition)beanDef).getMetadata();
        } else {
            try {
                MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className);
                metadata = metadataReader.getAnnotationMetadata();
            } catch (IOException ex) {
                return false;
            }
        }
        return metadata.hasAnnotation("org.springframework.boot.autoconfigure.SpringBootApplication");
    }

    public final Environment getEnvironment() {
        if (this.environment == null)
            this.environment = new StandardEnvironment();
        return this.environment;
    }

    private ResourcePatternResolver getResourcePatternResolver() {
        if (this.resourcePatternResolver == null)
            this.resourcePatternResolver = new PathMatchingResourcePatternResolver();
        return this.resourcePatternResolver;
    }

    protected String resolveBasePackage(String basePackage) {
        return ClassUtils.convertClassNameToResourcePath(getEnvironment().resolveRequiredPlaceholders(basePackage));
    }

    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(MapperRegisterStore.class);
        beanDefinitionBuilder.setScope("singleton");
        AbstractBeanDefinition abstractBeanDefinition = beanDefinitionBuilder.getBeanDefinition();
        registry.registerBeanDefinition(Objects.requireNonNull(abstractBeanDefinition.getBeanClassName()), abstractBeanDefinition);
    }

    /**
     * 扫描被@Key注解标识的实体类
     * @param beanFactory
     * @return
     */
    public Map<Class<?>, Field> parseBeanDefinitionCandidate(ConfigurableListableBeanFactory beanFactory) {
        Map<Class<?>, Field> tableClassz = new HashMap<>();
        List<BeanDefinitionHolder> candidates = new ArrayList<>();
        String[] candidateNames = beanFactory.getBeanDefinitionNames(); // 从BeanFactory中取出已经构建的BeanDefinition
        for (String name : candidateNames) {
            BeanDefinition beanDef = beanFactory.getMergedBeanDefinition(name);
            if (checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) // 找出@SpringBoot注解的主类,主类上使用@Key@KeyGenerator("com.xxx.xx.xx.system.notice.domain.entity")
                candidates.add(new BeanDefinitionHolder(beanDef, name));
        }
        String basePackage = parseCandidateBasePackages(candidates); // 获取主类上的@KeyGenerator注解中的值,该值为实体类的包路径
        Set<BeanDefinition> bfs = scanTableInfo(basePackage); // 根据找到的实体类的包路径扫描实体类存放的地方,并且将下面所有的实体类扫描出来
        for (BeanDefinition bd : bfs) { // 遍历所有实体类
            try {
                Class<?> clz = Class.forName(bd.getBeanClassName());
                ReflectionUtils.doWithFields(clz, ff -> {
                    Annotation[] annotations = ff.getAnnotations();
                    for (Annotation annotation : annotations) {
                        if (annotation instanceof Key) // 判断实体类上的字段是否带有@Key注解,如果带有那么该字段便是由@Key注解标记的实体主键
                            tableClassz.put(clz, ff);
                    }
                });
            } catch (Throwable e) {
                throw new BeanDefinitionStoreException("Failed to parse entity class [" + bd
                        .getBeanClassName() + "]", e);
            }
        }
        return tableClassz;
    }

    private Set<BeanDefinition> scanTableInfo(String basePackage) {
        Set<BeanDefinition> candidates = new LinkedHashSet<>();
        try {
            String packageSearchPath = "classpath*:" + resolveBasePackage(basePackage) + '/' + "**/*.class";
            Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
            for (Resource resource : resources) {
                try {
                    MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
                    ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                    sbd.setSource(resource);
                    candidates.add(sbd);
                } catch (Throwable ex) {
                    throw new BeanDefinitionStoreException("Failed to read candidate entity class: " + resource, ex);
                }
            }
        } catch (IOException ex) {
            throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
        }
        return candidates;
    }

    public String parseCandidateBasePackages(List<BeanDefinitionHolder> candidates) {
        for (BeanDefinitionHolder holder : candidates) {
            BeanDefinition bd = holder.getBeanDefinition();
            try {
                KeyGenerator annotation = AnnotationUtils.findAnnotation(Class.forName(bd.getBeanClassName()), KeyGenerator.class);
                if (Objects.nonNull(annotation))
                    return annotation.value();
            } catch (BeanDefinitionStoreException ex) {
                throw ex;
            } catch (Throwable ex) {
                throw new BeanDefinitionStoreException("Failed to parse configuration class [" + bd
                        .getBeanClassName() + "]", ex);
            }
        }
        return null;
    }

    /**
     * BeanFactoryPostProcessor.postProcessBeanFactory方法实现,其目的是触发实体类的@Key注解扫描
     * 并将扫描结果保存至MapperRegisterStore中,MapperRegisterStore中储存了类的类型+@Key注解的主键字段
     * @param beanFactory the bean factory used by the application context
     * @throws BeansException
     */
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        Map<Class<?>, Field> fieldMap = parseBeanDefinitionCandidate(beanFactory); // 调用解析BeanDefinition方法
        MapperRegisterStore mapperRegisterStore = beanFactory.getBean(MapperRegisterStore.class); // 从BeanFactory容器中获取MapperRegisterStore实例
        mapperRegisterStore.putAll(fieldMap);
    }
}

实体类扫描流程逐一分析

1、Spring BeanFactory生命周期:

  触发时机->postProcessBeanFactory,了解Spring启动流程的都知道,Spring在启动过程中定义了很多Hook,同时也为整个Bean的创建到销毁定义了生命周期,那么BeanFactory工厂呢?

当然,BeanFactory也生命周期,而我们恰恰就是在这个BeanFactory生命周期的触发点上定义了实现

BeanFactoryPostProcessor 该定义实现最终会在Spring启动流程中Refresh()下的
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
调用触发,我们可以在这个节点上定义扫描实体类的实现;因为,这个节点刚好是BeanFactory创建完成BeanDefinition构建成功、Bean未实例化前
@FunctionalInterface
public interface BeanFactoryPostProcessor {

    /**
     * Modify the application context's internal bean factory after its standard
     * initialization. All bean definitions will have been loaded, but no beans
     * will have been instantiated yet. This allows for overriding or adding
     * properties even to eager-initializing beans.
     * @param beanFactory the bean factory used by the application context
     * @throws org.springframework.beans.BeansException in case of errors
     */
    void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;

}

2、BeanDefinitionRegistryPostProcessor 接口:

  该接口继承于上面的接口,同样也是在postProcessBeanFactory方法中触发;注意该接口中的唯一实现方法入参为BeanDefinitionRegistry ,我们可以利用BeanDefinitionRegistry往Spring IOC中注入自定义的BeanDefinition,然后再由Spring完成实例和初始化。

我们的实现代码中就实现了该接口,并且往里注入一个名为MapperRegisterStore的BeanDefinition,让我们一起来看看这个MapperRegisterStore的定义

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {

    /**
     * Modify the application context's internal bean definition registry after its
     * standard initialization. All regular bean definitions will have been loaded,
     * but no beans will have been instantiated yet. This allows for adding further
     * bean definitions before the next post-processing phase kicks in.
     * @param registry the bean definition registry used by the application context
     * @throws org.springframework.beans.BeansException in case of errors
     */
    void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;

}

该定义非常的简单,我们继承了HashMap,并指定了该泛型;由该定义存储了K,V结构,K为类的类型、V为类的字段属性;这里我们用来存储扫描的实体类的类型并且对应的@Key注解的字段,后续利用该字段可以通过反射的方式注入自增ID的属性

/**
 * @Author: Song L.Lu
 * @Since: 2023-05-30 15:15
 **/
public class MapperRegisterStore extends HashMap<Class<?>, Field> {
    private static final long serialVersionUID = -3863847035136313223L;
    public Field put(Class<?> k, Field v) {
        if (Objects.nonNull(k) && Objects.nonNull(v)) {
            return put(k, v);
        }
        return null;
    }
}

下面是自定义注入MapperRegisterStore的实现

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(MapperRegisterStore.class);
        beanDefinitionBuilder.setScope("singleton");
        AbstractBeanDefinition abstractBeanDefinition = beanDefinitionBuilder.getBeanDefinition();
        registry.registerBeanDefinition(Objects.requireNonNull(abstractBeanDefinition.getBeanClassName()), abstractBeanDefinition);
    }

3、扫描实体类路径:

  分析完了触发点,接下来我们接着分析parseBeanDefinitionCandidate方法;如下列代码所示,该方法主要调用了checkConfigrationClassCandidate来提取由@SpringBootApplication注解的主类,并且从主类上获取由@KeyGenerator注解定义的实体类包路径;通过包路径再去通过scanTableInfo方法扫描出来所有的实体类Class,这里代码实现时利用了MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory()实现,该类在ResourceLoader的基础上增强了缓存,让已经加载过的资源不用重复加载;最终再由ScannedGenericBeanDefinition 将转为的元数据转为BeanDefinition

private Set<BeanDefinition> scanTableInfo(String basePackage) {
        Set<BeanDefinition> candidates = new LinkedHashSet<>();
        try {
            String packageSearchPath = "classpath*:" + resolveBasePackage(basePackage) + '/' + "**/*.class";
            Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
            for (Resource resource : resources) {
                try {
                    MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
                    ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                    sbd.setSource(resource);
                    candidates.add(sbd);
                } catch (Throwable ex) {
                    throw new BeanDefinitionStoreException("Failed to read candidate entity class: " + resource, ex);
                }
            }
        } catch (IOException ex) {
            throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
        }
        return candidates;
    }

4、找到@Key注解的字段:

  由上面的scanTableInfo方法我们得到了所有实体类BeanDefinition集合,接下来通过遍历这些类的字段就可以得到由@Key注解的字段,并且将字段put到MapperRegisterStore中

for (BeanDefinition bd : bfs) { // 遍历所有实体类
            try {
                Class<?> clz = Class.forName(bd.getBeanClassName());
                ReflectionUtils.doWithFields(clz, ff -> {
                    Annotation[] annotations = ff.getAnnotations();
                    for (Annotation annotation : annotations) {
                        if (annotation instanceof Key) // 判断实体类上的字段是否带有@Key注解,如果带有那么该字段便是由@Key注解标记的实体主键
                            tableClassz.put(clz, ff);
                    }
                });
            } catch (Throwable e) {
                throw new BeanDefinitionStoreException("Failed to parse entity class [" + bd
                        .getBeanClassName() + "]", e);
            }
        }
/**
     * 扫描被@Key注解标识的实体类
     * @param beanFactory
     * @return
     */
    public Map<Class<?>, Field> parseBeanDefinitionCandidate(ConfigurableListableBeanFactory beanFactory) {
        Map<Class<?>, Field> tableClassz = new HashMap<>();
        List<BeanDefinitionHolder> candidates = new ArrayList<>();
        String[] candidateNames = beanFactory.getBeanDefinitionNames(); // 从BeanFactory中取出已经构建的BeanDefinition
        for (String name : candidateNames) {
            BeanDefinition beanDef = beanFactory.getMergedBeanDefinition(name);
            if (checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) // 找出@SpringBoot注解的主类,主类上使用@Key@KeyGenerator("com.xxx.xx.xx.system.notice.domain.entity")
                candidates.add(new BeanDefinitionHolder(beanDef, name));
        }
        String basePackage = parseCandidateBasePackages(candidates); // 获取主类上的@KeyGenerator注解中的值,该值为实体类的包路径
        Set<BeanDefinition> bfs = scanTableInfo(basePackage); // 根据找到的实体类的包路径扫描实体类存放的地方,并且将下面所有的实体类扫描出来
        for (BeanDefinition bd : bfs) { // 遍历所有实体类
            try {
                Class<?> clz = Class.forName(bd.getBeanClassName());
                ReflectionUtils.doWithFields(clz, ff -> {
                    Annotation[] annotations = ff.getAnnotations();
                    for (Annotation annotation : annotations) {
                        if (annotation instanceof Key) // 判断实体类上的字段是否带有@Key注解,如果带有那么该字段便是由@Key注解标记的实体主键
                            tableClassz.put(clz, ff);
                    }
                });
            } catch (Throwable e) {
                throw new BeanDefinitionStoreException("Failed to parse entity class [" + bd
                        .getBeanClassName() + "]", e);
            }
        }
        return tableClassz;
    }

至此,实体类与数据表的的主键映射的流程解析完毕,接下来将解析Mybatis下的自定义的拦截器,这个拦截器也是实现自定义ID自增的关键 


5、Mybatis拦截器:

熟悉Mybatis都应该都听过Myabtis的自定义拦截器,该拦截器允许在Executor、ParameterHandler、ResultSetHandler、StatementHandler处进行拦截,实现原理是利用JDK的动态代理将所有Interceptor实现类对前面四个接口下的
任意方法进行代理,最后会产生一个代理后的包装调用链,这个链的最尾部就是实际Executor、ParameterHandler、ResultSetHandler、StatementHandler中的任意一个需要拦截的方法,那么之前的Interceptor接口的实现类则会对传入
的参数或结果进行拦截处理;我们利用这个特性在update或insert方法真正执行之前先执行我们自定义的拦截器,并对这个拦截中传入的参数进行处理,传入我们需要自增长的ID的值。
拦截器的实现首先对传入的参数进行提取,取出主要的插入对象obj,利用之前在Spring启动时由@Key扫描到的实体类主键字段。通过反射的方式将自定义的ID生成策略赋值给实体类的主键ID

/**
 * @Author: Song L.Lu
 * @Since: 2024-01-18 11:15
 **/
@Intercepts({@Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
)})
public class GeneratorInterceptor implements Interceptor {
    private Generator generator;
    private MapperRegisterStore mapperRegisterStore;

    public GeneratorInterceptor(Generator generator, MapperRegisterStore mapperRegisterStore) {
        this.generator = generator; // 注入自定义增长器的具体实现
        this.mapperRegisterStore = mapperRegisterStore; // 注入存储实体类主键的数据结构类
    }

    /**
     * 实现拦截器方法
     * @param invocation
     * @return
     * @throws Throwable
     */
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        if (args.length == 0) {
            throw new BindingException("Mapper代理对象没有传入的参数!");
        } else {
            MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
            Object obj = invocation.getArgs()[1];
            Class<?> clz = obj.getClass();
            Field id = this.mapperRegisterStore.get(clz);
            if (Objects.nonNull(id)) {
                ReflectionUtils.makeAccessible(id);
                ReflectionUtils.setField(id, obj, this.generator.nextId()); // 调用ID自增长器的nextId方法,用来获取自定义的ID,这里使用的是Twitter的雪花
            }

            SqlCommandType commandType = mappedStatement.getSqlCommandType();
            boolean existsInsert = false;
            switch (commandType) { // 根据Mybatis原先解析出的sql命令类型确定是插入还是更新
                case INSERT:
                    existsInsert = true;
                    break;
                case UPDATE:
                    existsInsert = false;
                    break;
                default:
                    throw new BindingException("参数绑定,发生未知错误!");
            }

            // 这里调用自定义增长器实现额外的属性添加,existsInsert标记当前的操作时插入还是更新
            generator.propertyValues(obj, existsInsert);
            return invocation.proceed();
        }
    }

6、Generator接口:
  Generator接口主要为引入的插件自定义实现自增长的策略实现,这里演示使用雪花算法

/**
 * @Author: Song L.Lu
 * @Since: 2023-05-31 09:51
 **/
public interface Generator<T> {
    T nextId();
    void propertyValues(Object c, boolean existsInsert);
}

7、Generator的实现

/**
 * @Author: Song L.Lu
 * @Since: 2023-06-01 09:32
 **/
@Component
public class SnowflakeIdGenerator implements Generator<Long> {
    private SnowflakeIdWorker snowflakeIdWorker;

    @Autowired
    public void setSnowflakeIdWorker(SnowflakeIdWorker snowflakeIdWorker) {
        this.snowflakeIdWorker = snowflakeIdWorker; // 注入雪花算法的生成器
    }

    @Override
    public Long nextId() {
        return snowflakeIdWorker.nextId();
    }

    @Override
    public void propertyValues(Object c, boolean existsInsert) {
        if(c instanceof DbBaseEntity){
            ((DbBaseEntity) c).populate(existsInsert); // 这里定义了额外属性的赋值
        }
    }

}
        实现自定义的ID增长器的方式有很多种,这里只是本身的一种思路,我觉得如何去实现的并不是太重要,重要的是如何开拓实现的思路,实现一种变通,增强思维能力和举一反三的思维模式才是最主要的。
源码地址:https://gitee.com/luxsong/mybatis-generator-starter
下面是使用 AbstractRoutingDataSource 和 MyBatis 拦截器实现动态切换数据源的示例代码: 首先,需要自定义一个继承 AbstractRoutingDataSource 的类,并实现 determineCurrentLookupKey 方法,该方法用于返回当前数据源的 key: ```java public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<>(); public static void setDataSourceKey(String key) { dataSourceKey.set(key); } @Override protected Object determineCurrentLookupKey() { return dataSourceKey.get(); } } ``` 在 Spring 配置文件需要配置两个数据源,并将 DynamicDataSource 设置为默认数据源: ```xml <bean id="dataSource1" class="org.apache.commons.dbcp2.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/db1"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="dataSource2" class="org.apache.commons.dbcp2.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/db2"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="dynamicDataSource" class="com.example.DynamicDataSource"> <property name="defaultTargetDataSource" ref="dataSource1"/> <property name="targetDataSources"> <map> <entry key="db1" value-ref="dataSource1"/> <entry key="db2" value-ref="dataSource2"/> </map> </property> </bean> ``` 接下来,需要实现一个继承于 MyBatis 的 Interceptor 接口的拦截器类,该类用于在执行 SQL 语句前切换数据源: ```java @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class DynamicDataSourceInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); String dataSourceKey = getDataSourceKey(mappedStatement); if (dataSourceKey != null) { DynamicDataSource.setDataSourceKey(dataSourceKey); } return invocation.proceed(); } private String getDataSourceKey(MappedStatement mappedStatement) { String dataSourceKey = null; // 从 Mapper 方法上获取数据源 key if (mappedStatement != null) { String id = mappedStatement.getId(); if (id.startsWith("com.example.mapper1")) { dataSourceKey = "db1"; } else if (id.startsWith("com.example.mapper2")) { dataSourceKey = "db2"; } } return dataSourceKey; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // do nothing } } ``` 最后,需要在 Spring 配置文件配置该拦截器: ```xml <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dynamicDataSource"/> <property name="plugins"> <array> <bean class="com.example.DynamicDataSourceInterceptor"/> </array> </property> </bean> ``` 这样,就可以在 Mapper 方法上使用 @DataSource("db1") 或 @DataSource("db2") 注解来指定使用哪个数据源了。例如: ```java @DataSource("db1") List<User> getUserList(); @DataSource("db2") int addUser(User user); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值