在springboot集成mybatis项目中,我们只需要在启动类上加上@MapperScan注解,让spring在启动时去扫描某路径下的所有mapper接口,然后就能实现mapper接口对应的增删改查功能,但mapper类,只是一个接口,我们并未为其写具体的实现,那么它是怎么被实例化的呢?本文就从源码的角度来解析这一过程。通过本文的学习,我们还能举一反三的让我们以后写自己的功能时,更好的和spring进行整合。
理论上而言,应该是这样的:
@MapperScan注解告诉spring去扫描路径下的所有mapper,然后根据mapper生成对应的实现类,并将实现类注入到spring容器中。但mapper接口的实现类真的需要多个吗?其实不需要,因为mapper实现类的功能总体来说都是获取sql执行sql。因此为mapper生成实现类时,可以生产一个代理类,在代理类中实现获取sql、执行sql的功能。
如果我们需要来实现这个功能应该如何做呢?以下代码为模拟代码。
实现步骤
定义一个基于jdk的动态代理类
为什么需要一个动态代理类?因为mapper是接口,接口本身是不能实例化的,因此需要为接口定义实现类,但如果只是一个简单的实现类,则我们需要为每一个mapper接口都定义实现类,这显然是不可能的;动态代理是解决这类问题的绝佳方案。但实现动态代理,可以有多种技术选型,如基于jdk动态代理、基于cglib的动态代理、基于原生字节码来实现动态代理等等。这里以jdk动态代理为例,进行模拟。
//第一个版本
public class MyProxy implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("toString")){
return "";
}
System.out.println("代理前"+method.getName());
Object result = method.invoke(, args);
System.out.println("代理后"+method.getName());
return result;
}
}
上述代码段中,Object result = method.invoke(, args);第一个参数是什么,本身应该是被代理对象实例。但我们的mapper只是接口,并没有实现类,哪怕是默认的实现类都没有,因此这里不能像普通的jdk代理那样做。
//第二个版本
public class MyProxy implements InvocationHandler {
private Class target;
public MyProxy(Class target){
this.target= target;
}
//方便获取代理对象
public Object getObject(){
return Proxy.newProxyInstance(target.getClassLoader(),new Class[]{target},this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("toString")){
return "";
}
System.out.println("代理前"+method.getName());
//实现sql的增删改查,
System.out.println("代理后"+method.getName());
return result;
}
}
实现sql的增删改查,主要包含如下步骤:
- 需要一个被代理的mapper接口的Class对象,因此需要使用构造方法传入进来
- 获取方法上的注解,假设@Select(“select id,user_name as userName from aimas_sys_user”)
- 使用相关技术来执行sql语句,如jdbc
- 返回sql执行结果
如何让自定义的动态代理类注入到spring容器中
一般将一个类注入到spring容器中,能想到的方法有很多,常用的有用@Component等注解、在config类中用@Bean注解等等。
但这些方法都只能一次注入一个bean到spring容器中。
这里我们使用另外一种日常开发过程中用得较少的FactoryBean接口来实现。
自定义类如MyFactoryBean实现FactoryBean接口,并重新方法。
public class MyFactoryBean implements FactoryBean {
private Class target;
private MyFactoryBean(Class target){
this.target = target;
}
//factorybean产生对象的方法
public Object getObject() throws Exception {
MyProxy myProxy = new MyProxy(target);
return myProxy.getObject();
}
@Override
public Class<?> getObjectType() {
return target;
}
@Override
public boolean isSingleton() {
return true;
}
}
上述factorybean不能使用@Component等注解修饰,因为需要为构造方法传参数;再者,我们需要使用这个factorybean创建N个代理对象,最好的方式就是类的路径扫描,就像@MapperScan一样。
定义scan
我们也可以参照@MapperScan来自定义scan,扫描某个路径下的所有mapper。代码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MyScannerRegistrar.class)
public @interface MyScan {
String[] value() default {};
}
只定义了MyScan注解,还需要为该注解写一个解析类,如上述代码中提到的MyScannerRegistrar
定义MyScannerRegistrar类
该类的功能是解析MyScan注解修饰的路径,扫描该路径下的所有mapper接口,并结合自定义的FactoryBean生成自定义的代理对象,并将生成好的代理对象注入到spring容器中。
public class MyScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//获取MyScan注解类的属性值
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MyScan.class.getName()));
List<String> basePackages = new ArrayList<String>();
for (String pkg : annoAttrs.getStringArray("value")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
try{
//扫描路径下的所有mapper接口名
Set<String> scan = scan(basePackages);
for(String s:scan){
//为mapper接口生成BeanDefinition,并将BeanDefinition保存到spring的beanDefinitionMap中,
// 这样在bean实例化的时候,会根据BeanDefinition实例化对应的bean
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class.getName());
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
Class clazz = Class.forName(s);
//为构造函数指定参数
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(clazz);
//向beanDefinitionMap注册beanDefinition
registry.registerBeanDefinition(clazz.getSimpleName().substring(0,1).toLowerCase()+clazz.getSimpleName().substring(1), beanDefinition);
}
}catch (Exception e){
e.printStackTrace();
}
}
public Set<String> scan(List<String> bases) throws Exception{
Set<String> set = new HashSet<>();
MetadataReaderFactory metaReader = new CachingMetadataReaderFactory(resourceLoader);
for(String base:bases) {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
ClassUtils.convertClassNameToResourcePath(new StandardEnvironment().resolveRequiredPlaceholders(base)) + "/**.class";
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(packageSearchPath);
for (Resource r : resources) {
MetadataReader reader = metaReader.getMetadataReader(r);
System.out.println(reader.getClassMetadata().getClassName());
set.add(reader.getClassMetadata().getClassName());
}
}
return set;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
自定义MyScannerRegistrar类实现了ImportBeanDefinitionRegistrar接口,并重写了registerBeanDefinitions方法。在方法中需要自定义要引入对象的BeanDefinition对象,并将BeanDefinition对象注册到spring的beanFactory中去。
当然,mybatis集成spring的代码比上述代码要复杂得多,但核心逻辑基本一致,感兴趣的朋友可以去看看mybatis集成spring的代码。
核心类如下:
- @MapperScan,入口类
- MapperScannerRegistrar 负责扫描类路径,并将扫描到的类生成beanDefinition
- MapperFactoryBean:利用factorybean的方式生成代理类的bean,为生成beanDefinition做准备
- MapperProxyFactory:使用jdk动态代理生成代理类,为factorybean生成bean做准备
- MapperProxy:实现对mapper接口的动态代理,为jdk生成代理类做准备