前提:
在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框架的部分源代码之后,自己简单实现了相关功能的代码。希望对你有帮助
代码整体结构
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经过特殊处理后的产物。
主启动类:
使用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(); } }
在使用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包下的接口文件。
以Spring为起点,Spring的@ComponentScan注解,只能用于扫描@Component相关的组件,使其能够成为Bean对象。但接口是不能直接实例化为一个对象,即无法直接成为Bean,所以自定义一个注解来扫描mapper下面的接口文件。
想要处理接口文件,就先要获取到这些文件,但是Spring又没办法帮我们获取,我们只能自己去获取
// 引入封装mapper层接口文件的配置类 @Import(ProxyMapperBDRegistrar.class) @Retention(RetentionPolicy.RUNTIME) public @interface MobianMapperScan { // 定义一个value属性,用于接受指定的包路径 String value() default ""; }
此处使用实现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的容器中。
那么我们就只剩下最后一步了,就是如何去封装对象。
实现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对象的操作就全部完成了。
第一行:在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格式异常"); } }
顺着前面的逻辑,我们来对比一下框架源码是如何完成的。当然我们这里主要对比的是那三个核心的连接类。
MyBatis中含有@Mapper注解,用来作为mapper接口的标识。想要批量的扫描接口,那么对应的源代码只能是在mybatis-spring.jar源码中。
在该注解的注释上有这么一句话:
Use this annotation to register MyBatis mapper interfaces when using Java Config.
表示我们在使用Java配置类启动Spring容器时,可以使用这个注解去注册MyBatis的mapper接口。即这是mybatis-spring.jar中为我们提供的扫描接口文件的注解。与前文我们自定义的注解作用相同
同样的,结合@Import注解,实现了ImportBeanDefinitionRegistrar接口,重写registerBeanDefinitions方法,遍历@MapperScan注解获取到的mapper文件,将其注入到Spring的容器中
// 方法调用流程 registerBeanDefinitions --> doScan --> processBeanDefinitions(beanDefinitions) --> definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)
循环遍历初始化每一个接口的封装对象
实现FactoryBean接口,表示该类为Spring的一个Bean
该类完成初始化以后即是Spring的一个类,又包含了接口信息
我们可以看到这里的getObject方法内部只是进行了一个方法的调用,且该方法指向的是MyBatis的代码。我们前面演示的动态代理这个接口的方法呢?
由此我们可以确定,将接口封装为一个代理对象是MyBatis自带的功能。我们继续往下看
getMapper方法的调用关系
invoke方法就是代理对象具体的代理逻辑。
注意:不同版本的jar包代码略有差异,请自行参考源码学习