@Autwired自动注入XxxMapper接口原理(含mybstis-spring.jar源码)






一、引言

前提:

在Spring容器中,传统意义上的对象,都变成了Bean,当然这里也引出了Bean的依赖注入概念,这个和new对象时构造方法参数的填充是一个意思。

即我们在使用对象时,依然可以采用new的方式(创建原型bean就new对象,创建单例bean就将实体类编写为单例模式),但该方式缺少一个统一管理的平台,各个Bean之间联系较弱,管理相对比较困难。



提问:

这里就有一个问题,在service层与mapper层的交互上,我们只需要使用一个简单的@Autowired注解,就能够将mapper层的接口与service层进行连接,以至于我们在使用mapper层具体接口调用具体方法时,也就成为了一个理所应当的操作。

可mapper层的文件都是接口,接口能实例化?

既然接口不能实例化,那么接口就不能直接成为Spring容器中的Bean,那么mapper层文件又为何能以@Autowired的形式进行自动注入呢?



是Spring中接口能实例化呢?还是Spring完成了对相关接口的封装,使其能够实例化呢?或许还有其他可能。

当然我个人更偏向于第二种(典型的马后炮),结合mybatis-spring、spring、mybatis框架的部分源代码之后,自己简单实现了相关功能的代码。希望对你有帮助






二、代码编写

代码整体结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sXnmVEig-1626592843470)(仿spring-mybatis包完成mapper封装.assets/image-20210717223818241.png)]




1、业务类

service类:

@Component
public class StudentService {

	@Autowired
	private TeacherMapper teacherMapper;

	@Autowired
	private StudentMapper studentMapper;

	public void showInfo(){
		teacherMapper.findInfo();
		studentMapper.findInfo();
	}

}

两个mapper接口:

public interface StudentMapper {
	void findInfo();
}

public interface TeacherMapper {
	void findInfo();
}



不要忘记了为什么出发。

既然StudentMapper.java文件是一个接口,那么它就不能完成实例化,继而无法成为Spring容器中的Bean。那么我们为什么又能使用@Autowired注解获取到Spring容器中的以xxMapper为名字的Bean呢?



后面的步骤都是为了验证我前文说到的第二种猜想,即此处以xxMapper为名字的Bean,并不是我们在IDEA中按住Ctrl跳转过去的XxxMapper接口,而是对XxxMapper经过特殊处理后的产物。




2、Spring容器启动类

主启动类:

使用AnnotationConfigApplicationContext的方式启动Spring容器。

后续再完成相关方法的调用(类比我们项目中的业务代码)

public class MainTest1 {
	public static void main(String[] args) {

		// 根据配置类启动Spring容器
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ContextConfig.class);

        // 获取对应的Bean,并完成相关方法的调用
		StudentService studentService = (StudentService)context.getBean("studentService", StudentService.class);
		studentService.showInfo();
	}
}




3、连接类(重点)

3.1 ContextConfig核心配置文件类

在使用ClassPathXmlApplicationContext类的方式启动Spring容器时,是使用类似于applicationcontext.xml这样的xml文件作为参数,继而完成Spring容器的初始化

由于此处使用的是AnnotationConfigApplicationContext类的方式启动Spring容器,其对应的初始化参数为一个配置类。即需要创建一个ContextConfig.class配置类

// 使用注解标记,表示该类为一个配置类
@Configuration
// 扫描对应的路径,获取对应的bean
@ComponentScan("pers.mobian.springsixth")
// 自定义一个扫描注解,用来扫描接口,类似于@ComponentScan注解
@MobianMapperScan("pers.mobian.springsixth.mapper")
public class ContextConfig {

	// 编写项目涉及的其他配置项
}

前文说过,mapper包下的接口不能实例化,以至于Spring的扫描Bean时,它会剔除mapper文件下的接口文件,我们常见的使用了@Controller、@Service等注解的类都会成为Spring中的一个个Bean。

既然如此我们就自已定义一个注解,完成类似于@ComponentScan注解的功能,用于扫描mapper包下的接口文件。



3.2 MobianMapperScan自定义注解类

以Spring为起点,Spring的@ComponentScan注解,只能用于扫描@Component相关的组件,使其能够成为Bean对象。但接口是不能直接实例化为一个对象,即无法直接成为Bean,所以自定义一个注解来扫描mapper下面的接口文件。

想要处理接口文件,就先要获取到这些文件,但是Spring又没办法帮我们获取,我们只能自己去获取

// 引入封装mapper层接口文件的配置类
@Import(ProxyMapperBDRegistrar.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface MobianMapperScan {

    // 定义一个value属性,用于接受指定的包路径
   String value() default "";
}



3.3 ProxyMapperBDRegistrar注册类

此处使用实现ImportBeanDefinitionRegistrar接口的方式来完成Bean的动态注册,该接口只能使用@Import注解来加载,此时可以将该类间接的理解为配置类。

public class ProxyMapperBDRegistrar implements ImportBeanDefinitionRegistrar {

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

		// 获取注解上的值
		// 该类被Import注解所引用,且Import注解使用在@MobianMapperScan注解上,即@MobianMapperScan注解就能够获取对应的路径值
		Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(MobianMapperScan.class.getName());

		// 有了对应的路径,就能够利用spring去扫描对应路径下的class,继而将class存放到对应的集合中,再完成接下来的动作
		System.out.println(annotationAttributes.get("value"));

		// 模拟后期扫描到的class文件,并且将class文件存放到对应的集合中的场景(包名已经获取到,获取mapper文件就很方便)
		ArrayList<Class> mappers = new ArrayList<>();
		mappers.add(TeacherMapper.class);
		mappers.add(StudentMapper.class);

        // 循环遍历每一个的mapper接口文件
		for (Class mapper : mappers) {

			// 创建一个默认的bd,并设置对应的bd的类型。
            // 此处的类型设置为mapper接口封装处理后的对象,后文会定义该处理类
			AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
			beanDefinition.setBeanClass(MobianFactoryBean.class);

			// 使用对应的构造方法,完成属性的初始化。
			// 此处的属性为,我们扫描到的每一个mapper接口文件
			beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(mapper);

			// 将我们的bd注入到spring的容器中
			registry.registerBeanDefinition(mapper.getName(), beanDefinition);
		}
	}
}

该类的功能可以概括为,获取到对应的mapper接口文件,并且完成接口的封装,再将封装好的新对象注册到Spring容器中。for循环内部的操作就是将封装后的新对象注册到Spring的容器中。

那么我们就只剩下最后一步了,就是如何去封装对象。



3.4 MobianFactoryBean接口代理类

实现FactoryBean接口,重写getObject方法和getObjectType方法,使该类成为Spring容器的一个Bean。这里涉及Spring创建Bean的不同实现方式的知识点,请自行补充。

既然接口不能直接实例化,那我们就使用动态代理,只要我们的新类含有对应的接口信息即可。此处直接将动态代理的方法体写在getObject方法体中。不熟悉动态代理的知识点,请自行补充。

此处采用JDK的动态代理,完成相关的操作

public class MobianFactoryBean implements FactoryBean {

	// mapper文件,使用成员变量,能将动态代理的参数中的接口名字写活
	private Class mapper;

    // 使用构造方法完成成员变量的初始化
	public MobianFactoryBean(Class mapper) {
		this.mapper = mapper;
	}

	@Override
	public Object getObject() throws Exception {

        // 使用jdk的动态代理,完成mapper对象的封装,使返回的对象成为了原生接口的封装体,即返回一个内含接口信息的新对象
		Object proxyObject = Proxy.newProxyInstance(MobianFactoryBean.class.getClassLoader(), new Class[]{mapper}, new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                // 执行代理后的逻辑
				System.out.println("执行了对应的代理逻辑...");
				System.out.println(method);
				System.out.println(method.getName());

				return null;
			}
		});

        // 返回代理后的对象,该对象已经不是接口了,是一个含有接口信息的Object类,可以完成实例化
		return proxyObject;
	}

	@Override
	public Class<?> getObjectType() {
		return mapper;
	}
}

在动态代理内部,我们可以获取到对应的方法,以及其他重要信息。至此接口封装成一个能够被Spring识别的Bean对象的操作就全部完成了。



3.5 总结
  • ContextConfig类:用于当作Spring容器启动的配置文件。
  • @MobianMapperScan注解:扫描mapper层所有的接口文件。
  • MobianFactoryBean类:对mapper层每一个xxxMapper.java接口文件进行封装,由原来的接口,变成一个代理对象,在代理对象内部含有原来的接口文件的所有信息。使mapper接口文件能够完成实例化,继而能够成为Spring中的一个Bean。
  • ProxyMapperBDRegistrar类:与@Import注解配合使用,达到配置类的效果。实现ImportBeanDefinitionRegistrar接口,重写相关的方法,再方法内部将前面封装好的代理对象类,依次注册到Spring容器中。




4、测试结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cYlHkUHH-1626592843472)(仿spring-mybatis包完成mapper封装.assets/image-20210718121008647.png)]


第一行:在BeanDefinitionRegistrar注册类中,我们利用自定义的注解获取配置的包路径(3.3节)

第二行:由于使用动态代理完成相关接口的代理,那么当我们在调用接口中方法时,我们使用的对象就已经是包装过后的含有接口信息的新对象,继而执行我们的代理逻辑。此处使用一句话做为输出

第三行,第四行:打印对应的方法信息。如果我们能够拿到方法的信息,就自然能够完成方法的后续操作。





补充获取到接口中方法后MyBatis如何与SQL语句进行连接:(与本博文内容关联不大,可跳过)

承接上面第三、四行的逻辑,MyBatis中我们写SQL分为两种方式,使用注解和配置一个mapper.xml文件。以注解为例,我们在mapper接口代理类中,使用了动态代理,我们的代理逻辑可以写在invoke方法内。我们可以添加相关的处理逻辑,拿到接口方法上面注解内部的SQL语句,再根据参数完善出最终的SQL语句,简易代码如下:

public interface BlogMapper {
    
  @Select("select * from blog where id = #{id}")
  List<Map<String, Object>> queryBlogListById(String id);

}
public class Demo {
  public static void main(String[] args) {
      
    // 动态代理我们的mapper接口
    BlogMapper blogMapper = (BlogMapper) Proxy.newProxyInstance(Demo.class.getClassLoader(), new Class<?>[]{BlogMapper.class}, new InvocationHandler() {
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Select annotation = method.getAnnotation(Select.class);
        if (annotation != null) {
          String[] value = annotation.value();
          //获取sql语句
          String sql = value[0];

          HashMap<String, Object> argsMap = new HashMap<>();
          if (args != null) {
            for (int i = 0; i < args.length; i++) {
              argsMap.put("id", args[i]);
            }

            // 传入sql和参数,完成解析
            String newSQL = parseSQL(sql, argsMap);
            //获取我们传入的参数
            System.out.println(sql);
            System.out.println(newSQL);
          }
        }
        return null;
      }
    });

    // 调用对应的sql语句
    blogMapper.queryBlogListById("11");
  }

    
  public static String parseSQL(String sql, Map<String, Object> argsMap) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < sql.length(); i++) {
      char s = sql.charAt(i);
      if (s == '#') {
        if (sql.charAt(++i) != '{') throw new RuntimeException("sql格式异常,缺少 { ");

        //解析参数名字
        String argName = parseParam(sql, i, sb);
        Object argValue = argsMap.get(argName);
        sb.append(argValue);
        return sb.toString();
      }
      sb.append(s);
    }
    return sb.toString();
  }

    
  public static String parseParam(String sql, int index, StringBuilder sqlFrag) {
    StringBuilder stringBuilder = new StringBuilder();
    index++;
    for (; index < sql.length(); index++) {
      char c = sql.charAt(index);
      if (c != '}') {
        stringBuilder.append(c);
        continue;
      }
      if (c == '}') {
        return stringBuilder.toString();
      }
    }
    throw  new RuntimeException("SQL格式异常");
  }
}






三、源码对比

顺着前面的逻辑,我们来对比一下框架源码是如何完成的。当然我们这里主要对比的是那三个核心的连接类。


1、@MobianMapperScan——@MapperScan

MyBatis中含有@Mapper注解,用来作为mapper接口的标识。想要批量的扫描接口,那么对应的源代码只能是在mybatis-spring.jar源码中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S3bWyDDs-1626592843474)(仿spring-mybatis包完成mapper封装.assets/image-20210718141921126.png)]


在该注解的注释上有这么一句话:

Use this annotation to register MyBatis mapper interfaces when using Java Config.



表示我们在使用Java配置类启动Spring容器时,可以使用这个注解去注册MyBatis的mapper接口。即这是mybatis-spring.jar中为我们提供的扫描接口文件的注解。与前文我们自定义的注解作用相同




2、ProxyMapperBDRegistrar——MapperScannerRegistrar

同样的,结合@Import注解,实现了ImportBeanDefinitionRegistrar接口,重写registerBeanDefinitions方法,遍历@MapperScan注解获取到的mapper文件,将其注入到Spring的容器中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jy4RiqQW-1626592843476)(仿spring-mybatis包完成mapper封装.assets/image-20210718142011821.png)]


// 方法调用流程
registerBeanDefinitions 
    --> doScan 
    --> processBeanDefinitions(beanDefinitions) 
    --> definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)



循环遍历初始化每一个接口的封装对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qInKqWCo-1626592843477)(仿spring-mybatis包完成mapper封装.assets/image-20210718142418681.png)]




3、MobianFactoryBean——MapperFactoryBean<T>

实现FactoryBean接口,表示该类为Spring的一个Bean

该类完成初始化以后即是Spring的一个类,又包含了接口信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3xxEvzC9-1626592843478)(仿spring-mybatis包完成mapper封装.assets/image-20210718142843280.png)]



我们可以看到这里的getObject方法内部只是进行了一个方法的调用,且该方法指向的是MyBatis的代码。我们前面演示的动态代理这个接口的方法呢?

由此我们可以确定,将接口封装为一个代理对象是MyBatis自带的功能。我们继续往下看



getMapper方法的调用关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nWENRcUY-1626592843479)(仿spring-mybatis包完成mapper封装.assets/image-20210718143316059.png)]



invoke方法就是代理对象具体的代理逻辑。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m6kLMMdD-1626592843479)(仿spring-mybatis包完成mapper封装.assets/image-20210718143430157.png)]




注意:不同版本的jar包代码略有差异,请自行参考源码学习

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

默辨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值