3.8 Spring xml方式整合第三方框架
3.8.1 Mybatis原始代码使用
1.配置Mybatis信息
创建Mybatis-config.xml文件,其内容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<!-- 设置数据源类型。由于表格存储JDBC驱动需要主动关闭后才能让进程退出,请根据实际使用选择合适的数据源类型。-->
<!-- 如果程序常驻执行,则您可以使用POOLED维护一个连接池;如果希望程序完成数据查询后退出,则只能使用UNPOOLED。-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_learn"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- 设置为映射配置文件的路径。-->
<package name="com.itheima.mapper"/>
</mappers>
</configuration>
在MySQL是8.0以上的版本时,driver为com.mysql.cj.jdbc.Driver
;在MySQL是5.0版本的时候driver是com.mysql.jdbc.driver
。以及在MySQL是8.0以上的版本时URL有时是需要添加时区等参数的,否则无法建立和数据库的链接,此次测试不需要添加这些参数,原因未知,可能原因是mysql驱动或者mybatis版本是新的所以解决了这个问题。
2.创建实体类
根据数据库的结构创建一个实体类。
数据库的结构如图:
实体类:
package com.itheima.pojo;
public class User {
private int id;
private String username;
private int age;
private String address;
/*
...
getter和setter方法,toString方法
...
*/
}
3.创建Mapper以及SQL语句
创建一个简单的Mapper接口:
public interface UserMapper {
List<User> findAll();
}
根据Mapper接口创建一个mapper的xml文件
<?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">
<!-- namespace: mapper接口的以用路径-->
<mapper namespace="com.itheima.mapper.UserMapper">
<!-- id: mapper接口中使用的方法
resultType: SQL返回的数据类型-->
<select id="findAll" resultType="com.itheima.pojo.User">
select * from USER_INFO
</select>
</mapper>
4.编写测试类
package com.itheima.test;
import com.itheima.mapper.UserMapper;
import com.itheima.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class MybatisTest {
public static void main(String[] args) throws IOException {
// 1.获取mybatis配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
// 2.根据mybatis配置文件创建sqlSessionFactory,并且获取一个sqlSession
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = builder.build(in);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.根据sqlSession生成userMapper的对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 4.执行userMapper中的方法并且将结果输出
List<User> all = userMapper.findAll();
for (User user : all){
System.out.println(user.toString());
}
}
}
MyBaits原始的使用方法比较繁琐,并且一般情况下是不会这么使用的,所以不需要刻意记住。
执行结果:
3.8.2 Spring整合Mybatis
1.导入Mybatis整合Spring的相关坐标
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.13.RELEASE</version>
</dependency>
2.编写Mapper和Mapper.xml
上个案例中已经写了对应的mapper以及mapper.xml,可以去上个案例查看。
3.配置SqlSessionFactoryBean和MapperScannerConfigurer
<!-- 数据源-->
<bean name="DataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_learn"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!-- SqlSessionFactory bean-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="DataSource"></property>
</bean>
<!-- MapperScannerConfigurer 扫描指定包,自动生成Mapper对象存入Spring容器-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.itheima.mapper"></property>
</bean>
SqlSessionFactoryBean需要数据源作为参数才可以创建。
4.编写测试代码
我们编写一个简单的UserService接口,它有一个简单的fun()方法,之后编写一个实现类UserServiceImpl实现这个接口。
UserServiceImpl中设置一个简单的Mapper属性,fun方法中使用Mapper中的方法实现数据库的查询功能。
public interface UserService {
public void fun();
}
public class UserServiceImpl implements UserService {
private UserMapper userMapper;
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public void fun() {
List<User> all = userMapper.findAll();
// lambda表达式实现遍历
all.forEach(
(user) -> {
System.out.println(user.toString());
}
);
}
}
之后在Spring的配置文件中添加对于这个Bean的配置信息:
<bean name="userService" class="com.itheima.service.impl.UserServiceImpl">
<property name="userMapper" ref="userMapper"></property>
</bean>
我们在从头到尾的过程中都没有配置UserMapper相关的Bean,但是在对UserService进行属性创建的时候却可以将userMapper作为bean传入,这是因为MapperScannerConfigurer会自己遍历对应路径下的mapper类并且生成bean存入Spring容器之中。
编写测试方法,调用userService的bean:
public class MybatisSpringTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) applicationContext.getBean("userService");
userService.fun();
}
}
测试结果:
3.8.3 Spring整合MyBatis的原理解析
整合包里提供了SqlSessionFactoryBean和一个扫描Mapper的配置对象。SqlSessionFactoryBean一旦被实例化,就开始扫描Mapper并且通过动态代理产生Mapper的实现类存储到Spring容器之中。其中主要相关的包括一下四个类:
SqlSessionFactoryBean:需要配置,用于提供SqlSessionFactory;
MapperScannerConfigurer:需要进行配置,用于扫描指定的mapper注册BeanDefiniftion;
MapperFactoryBean:Mapper的FactoryBean,获得指定Mapper时调用getObject方法(因为mybatis中mapper均是接口形式,想要生成对应的对象就要使用FactoryBean)
ClassPathMapperScanner:会将扫描到的Mapper Bean的注入状态是自动注入状态definition.setAutowireMode(2)
,所以MapperFactoryBean中的SqlSessionFactory会自动注入进去。
SqlSessionFactoryBean
SqlSessionFactoryBean在初始化阶段调用afterPropertiesSet()方法,创建SqlSessionFactory并且设置在属性中,创建的方法和原始的MyBatis代码相同,都是先创建SqlSessionFactoryBuilder之后使用SqlSessionFactoryBuilder创建SqlSessionFactory。
MapperScannerConfigurer的原理解析
Mapper扫描配置器。
我们在看Spring整合MyBatis时难免会有一个疑问:Mapper接口都是接口,不可以直接生成对象Bean,Spring容器是怎么生成的MapperBean呢?
答: 当Spring容器扫描到了Mapper文件,并且根据Mapper文件创建了BeanDefinition之后,会统一将Mapper的BeanDefinition文件中的BeanClass设置为MapperFactoryBean,并且将注入模式设置设置为自动注入。MapperFactoryBean是FactoryBean的子类,专门负责生成Mapper Bean。MapperFactoryBean将sqlSessionFactory作为属性,这个类是MyBatis原代码生成Mapper对象的工厂,之后Spring容器会自动将创建好的sqlSessionFactory Bean作为属性注入其中。之后在Spring容器的初始化阶段时,根据BeanDefinition创建实例时,会根据BeanDefinition将对应Mapper接口的Class传入作为构建参数,创建出这个Mapper专用的MapperFactoryBean,之后调用MapperFactoryBean的getObject方法,生成对应Mapper的代理bean。
MapperSessionFactoryBean的getObject方法:
MapperBean的创建过程:
3.8.4 Spring框架处理自定义命名空间原理解析
Spring框架处理XML中的自定义命名空间的步骤如下:
1. 获取XML中的自定义命名空间标签(element)
2. 根据元素自定义命名空间的URI获取自定义命名空间的解析器,使用获取到的解析器解析自定义的命名空间标签
1.获取XML中的自定义命名空间标签(element)
在Spring扫描完XML配置文件时,会将配置文件中的标签都获取到,并且依次解析(parse)这些标签。
这个获取XML中标签并且分析这些标签的过程处理代码如下:(此类为DefaultBeanDefinitionDocumentReader.java
)
/*类名: DefaultBeanDefinitionDocumentReader */
...
...
...
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for(int i = 0; i < nl.getLength(); ++i) {
Node node = nl.item(i);//获取xml中的节点信息
if (node instanceof Element) {
//如果节点类型为element则获取并且进行下一步处理
Element ele = (Element)node;
if (delegate.isDefaultNamespace(ele)) {
/*如果element为默认的命名空间(import、alias、bean、beans),
就使用默认命名空间解析的方式*/
this.parseDefaultElement(ele, delegate);
} else {
/*如果element不是默认的命名空间,
那么就启用自定义命名空间的处理方式*/
delegate.parseCustomElement(ele);
}
}
}
} else {
delegate.parseCustomElement(root);
}
}
...
...
...
2.根据元素自定义命名空间的URI获取自定义命名空间的解析器,使用获取到的解析器解析自定义的命名空间标签
Spring根据自定义命名空间的URI获取到此标签对应的解析器并且完成解析(parseDefaultElement()
)。
其代码实现如下(DefaultNamespaceHandlerResolver.java
):
...
@Nullable
public BeanDefinition parseCustomElement(Element ele) {
//调用自身类对应的parseCustomElement()方法
return this.parseCustomElement(ele, (BeanDefinition)null);
}
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
//获取标签对应的命名空间
String namespaceUri = this.getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
} else {
//获取命名空间对应的处理器(NamespaceHandler)
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
this.error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
} else {
//使用命名空间处理器来解析标签
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
}
}
...
其中,一般而言自定义命名空间的jar包中会在META-INF/spring.handlers
目录文件中存放命名空间URI对应的类(Spring会自动加载):
每个命名空间处理器(NamespaceHandler)都起码会有俩个方法:init() 和 parse()。
init()方法:来注册命名空间中每个标签的处理器
parse()方法:用来解析标签
3.8.5 案例:自制自定义命名空间
我们根据上述原理分析知道自定义一个命名空间需要以下几个条件:
1.在META-INF
目录下的spirng.handlers
以及spring.schemas
文件中有命名空间的键值对
2.拥有对应的xsd约束文件
3.有对应的解析器handler
4.各个对应标签均有对应的解析器
那么我们就不难根据以上条件来自己创建一个自定标签。此标签做一个简单的功能:将一个自定的BeanPostProcessor注册入Spring容器。
1.创建META-INF
目录,在其中创建spring.handlers
以及spring.schemas
文件,在其中填写好自定义命名空间对应的处理器以及对应的约束文件。
spring.handlers
文件:
spring.schemas
文件:
约束文件BugMakerAnno.xsd
对应的内容:
<?xml version="1.0" encoding="UTF-8" ?>
<xsd:schema xmlns="http://www.itheima.com/BugMaker"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.itheima.com/BugMakerAnno">
<xsd:element name="annotation-driven"></xsd:element>
</xsd:schema>
targetNamespace
属性必须为之前的自定义命名空间。
2.创建自定义命名空间对应的处理器类BugMakerAnnoHandler
BugMakerAnnoHandler.java:
package com.itheima.handlers;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class BugMakerAnnoHandler extends NamespaceHandlerSupport {
@Override
public void init() {
// 注册解析器
// “annotation-driven”为自定义命名空间中的标签名称,后面的时标签对应的处理类
this.registerBeanDefinitionParser("annotation-driven",new BugMakerAnnoParser());
}
}
我们可以看到,这个类结构十分简单,仅仅是在init()
方法中注册了标签对应的解析器。这是因为此类继承的父类NamespaceHandlerSupport
类,这个类的parse()
方法会根据传入的标签名称获取到已经在这个类中注册过的解析器,并且调用解析器的parse()
方法进行解析。
3.编写解析类
BugMakerAnnoParser
类:
public class BugMakerAnnoParser implements BeanDefinitionParser {
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
//在Spring容器中注册一个容器
// 1.创建BeanDefinition
BeanDefinition annoBeanDefinition = new RootBeanDefinition();
annoBeanDefinition.setBeanClassName("com.itheima.postProcessor.AnnoBeanPostProcessor");
// 2.将BeanDefiniton注册入Spring容器之中
parserContext.getRegistry().registerBeanDefinition("BugMakerAnnoBD",annoBeanDefinition);
return annoBeanDefinition;
}
}
BugMakerAnnoParser的代码逻辑即为创建一个BeanDefition,并且将创建的BeanDefition文件注册入Spring容器之中。
AnnoBeanPostProcessor
(BeanPostProcessor类):
public class AnnoBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("自定义标签注入的BeanPostProcessor已经运行");
return bean;
}
}
AnnoBeanPostProcessor
类的逻辑也很简单,只是在每个类实例化之前输入一个简单的日志。
4.在XML配置文件添加自定义命名空间以及对应的标签
5.运行项目