更新时间:2023-04-17 来源:黑马程序员 浏览量:
xml整合第三方框架有两种整合方案:
不需要自定义名空间,不需要使用Spring的配置文件配置第三方框架本身内容,例如:MyBatis;需要引入第三方框架命名空间,需要使用Spring的配置文件配置第三方框架本身内容,例如:Dubbo。
Spring整合MyBatis,之前已经在Spring中简单的配置了SqlSessionFactory,但是这不是正规的整合方式, MyBatis提供了mybatis-spring.jar专门用于两大框架的整合。
Spring整合MyBatis的步骤如下:
⚫ 导入MyBatis整合Spring的相关坐标;(请见资料中的pom.xml)
⚫ 编写Mapper和Mapper.xml;
⚫ 配置SqlSessionFactoryBean和MapperScannerConfigurer;
⚫ 编写测试代码。
配置SqlSessionFactoryBean和MapperScannerConfigurer:
<!--配置数据源--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="jdbc:mysql://localhost:3306/mybatis"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </bean> <!--配置SqlSessionFactoryBean--> <bean class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property> </bean> <!--配置Mapper包扫描--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.itheima.dao"></property> </bean>
编写Mapper和Mapper.xml
public interface UserMapper { List<User> findAll(); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.itheima.dao.UserMapper"> <select id="findAll" resulttype="com.itheima.pojo.User"> select * from tb_user </select> </mapper>
编写测试代码
ClassPathxmlApplicationContext applicationContext = new ClassPathxmlApplicationContext("applicationContext.xml"); UserMapper userMapper = applicationContext.getBean(UserMapper.class); List<User> all = userMapper.findAll(); System.out.println(all);
Spring整合MyBatis的原理剖析
整合包里提供了一个SqlSessionFactoryBean和一个扫描Mapper的配置对象,SqlSessionFactoryBean一旦被实例化,就开始扫描Mapper并通过动态代理产生Mapper的实现类存储到Spring容器中。相关的有如下四个类:
• SqlSessionFactoryBean:需要进行配置,用于提供SqlSessionFactory;
• MapperScannerConfigurer:需要进行配置,用于扫描指定mapper注册BeanDefinition;
• MapperFactoryBean:Mapper的FactoryBean,获得指定Mapper时调用getObject方法;
• ClassPathMapperScanner:definition.setAutowireMode(2) 修改了自动注入状态,所以
• MapperFactoryBean中的setSqlSessionFactory会自动注入进去。
配置SqlSessionFactoryBean作用是向容器中提供SqlSessionFactory,SqlSessionFactoryBean实现了FactoryBean和InitializingBean两个接口,所以会自动执行getObject() 和afterPropertiesSet()方法。
SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean{ public void afterPropertiesSet() throws Exception { //创建SqlSessionFactory对象 this.sqlSessionFactory = this.buildSqlSessionFactory(); } public SqlSessionFactory getObject() throws Exception { return this.sqlSessionFactory; } }
配置MapperScannerConfigurer作用是扫描Mapper,向容器中注册Mapper对应的MapperFactoryBean,MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor和InitializingBean两个接口,会在postProcessBeanDefinitionRegistry方法中向容器中注册MapperFactoryBean。
class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean{ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n")); } }
class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner { public Set<BeanDefinitionHolder> doScan(String... basePackages) { Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); if (beanDefinitions.isEmpty()) { } else { this.processBeanDefinitions(beanDefinitions); } } private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { //设置Mapper的beanClass是org.mybatis.spring.mapper.MapperFactoryBean definition.setBeanClass(this.mapperFactoryBeanClass); definition.setAutowireMode(2); //设置MapperBeanFactory 进行自动注入 } }
autowireMode取值:1是根据名称自动装配,2是根据类型自动装配。
class ClassPathBeanDefinitionScanner{ public int scan(String... basePackages) { this.doScan(basePackages); } protected Set<BeanDefinitionHolder> doScan(String... basePackages) { //将扫描到的类注册到beanDefinitionMap中,此时beanClass是当前类全限定名 this.registerBeanDefinition(definitionHolder, this.registry); return beanDefinitions; } }
UserMapper userMapper = applicationContext.getBean(UserMapper.class);
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { public MapperFactoryBean(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { this.sqlSessionTemplate = this.createSqlSessionTemplate(sqlSessionFactory); } public T getObject() throws Exception { return this.getSqlSession().getMapper(this.mapperInterface); } }
Spring 整合其他组件时就不像MyBatis这么简单了,例如Dubbo框架在于Spring进行整合时,要使用Dubbo提供的命名空间的扩展方式,自定义了一些Dubbo的标签。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"> <!--配置应用名称--> <dubbo:application name="dubbo1-consumer"/> <!--配置注册中心地址--> <dubbo:registry address="zookeeper://localhost:2181"/> <!--扫描dubbo的注解--> <dubbo:annotation package="com.itheima.controller"/> <!--消费者配置--> <dubbo:consumer check="false" timeout="1000" retries="0"/> </beans>
为了降低我们此处的学习成本,不在引入Dubbo第三方框架了,以Spring的 context 命名空间去进行讲解,该方式也是命名空间扩展方式。
需求:加载外部properties文件,将键值对存储在Spring容器中。
jdbc.url=jdbc:mysql://localhost:3306/mybatis jdbc.username=root jdbc.password=root
引入context命名空间,在使用context命名空间的标签,使用SpEL表达式在xml或注解中根据key获得value。
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:jdbc.properties" /> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="${jdbc.url}"></property> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <beans>
其实,加载的properties文件中的属性最终通过Spring解析后会被存储到了Spring容器的environment中去,不仅自己定义的属性会进行存储,Spring也会把环境相关的一些属性进行存储。
原理剖析解析过程,只能从源头ClassPathXmlApplicationContext入手,经历复杂的源码追踪,找到如下两个点:
1)在创建DefaultNamespaceHandlerResolver时,为处理器映射地址handlerMappingsLocation属性赋值,并加载命名空间处理器到MaphandlerMappings 中去。
this.handlerMappingsLocation = "META-INF/spring.handlers";
第一点完成后,Map集合handlerMappings就被填充了很多XxxNamespaceHandler,继续往下追代码
2)在DefaultBeanDefinitionDocumentReader的parseBeanDefinitions方法中,发现如下逻辑:
//如果是默认命名空间 if (delegate.isDefaultNamespace(ele)) { this.parseDefaultElement(ele, delegate); //否则是自定义命名空间 } else { delegate.parseCustomElement(ele); }
如果是默认命名空间,则执行parseDefaultElement方法
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { if (delegate.nodeNameEquals(ele, "import")) { this.importBeanDefinitionResource(ele); } else if (delegate.nodeNameEquals(ele, "alias")) { this.processAliasRegistration(ele); } else if (delegate.nodeNameEquals(ele, "bean")) { this.processBeanDefinition(ele, delegate); } else if (delegate.nodeNameEquals(ele, "beans")) { this.doRegisterBeanDefinitions(ele); } }
如果是自定义命名空间,则执行parseCustomElement方法
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { //解析命名空间 String namespaceUri = this.getNamespaceURI(ele); //获得命名空间解析器 NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); //解析执行的标签 return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); }
在执行resovle方法时,就是从MaphandlerMappings中根据命名空间名称获得对应的处理器对象,此处是ContextNamespaceHandler,最终执行NamespaceHandler的parse方法。
ContextNamespaceHandler源码如下,间接实现了NamespaceHandler接口,初始化方法init会被自动调用。由于context命名空间下有多个标签,所以每个标签又单独注册了对应的解析器,注册到了其父类NamespaceHandlerSupport的Map<String, BeanDefinitionParser>
parsers中去了。
public class ContextNamespaceHandler extends NamespaceHandlerSupport { public ContextNamespaceHandler() { } public void init() { this.registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser()); this.registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser()); this.registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser()); this.registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser()); this.registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser()); this.registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); this.registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser()); this.registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser()); } }
通过上述分析,我们清楚的了解了外部命名空间标签的执行流程,如下:
将自定义标签的约束 与 物理约束文件与网络约束名称的约束 以键值对形式存储到一个spring.schemas文件里,该文件存储在类加载路径的 META-INF里,Spring会自动加载到;
将自定义命名空间的名称 与 自定义命名空间的处理器映射关系 以键值对形式存在到一个叫spring.handlers文件里,该文件存储在类加载路径的 META-INF里,Spring会自动加载到;
准备好NamespaceHandler,如果命名空间只有一个标签,那么直接在parse方法中进行解析即可,一般解析结果就是注册该标签对应的BeanDefinition。如果命名空间里有多个标签,那么可以在init方法中为每个标签都注册一个BeanDefinitionParser,在执行NamespaceHandler的parse方法时在分流给不同的BeanDefinitionParser进行解析(重写doParse方法即可)。