介绍
原生的Mybatis框架是没有ID自增器,但例如国产的Mybatis Plus却是支持,不过,Mybatis Plus却是缺少了自定属性的填充;例如:我们需要自定义填充一些属性,updateDate、createDate等,这时Mybatis Plus自带的ID自增器就无法满足需求;这种时候我们就需要自定义的ID增加器,可以自定义ID增长策略同时还得支持更多的属性自定义扩展,当然,最好能做成插件形式为其他项目或者模块提供引入那就更新好了。
原理图
在开始之前我们先确定和分析主要的需求,了解了需求才能更好的制作
首先我们得确定主要的宗旨那就是实现ID自增长器,同时,还等保证该增长的灵活性和扩展性,让其他项目引入之后可以很灵活的更改增长策略,随意替换ID增长的实现。主要需求为下列几点:
- 自定义的ID增长策略,同时保证该特性灵活性可以随意替换
- 支持自定义的附带属性扩展增强
- 保证其他的项目引起使用时的绝对简单,最好能像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