Spring扩展原理
- 前言
- 案例
- 问题
- 紧接下文
前言
上次小编主要将了spring的refresh中invokeBeanFactoryPostProcessors方法的整体流程。大家可以回顾一下:这篇博客。那这次小编结合实际案例,到底第三方框架是如何进行扩展的。本期内容会比较复杂,希望小编能够讲清楚讲明白。
案例
spring是如何整合mybatis,首先我们看一下ssm整合示例。然后慢慢深究。
数据库表创建语句比较简单
CREATE TABLE `employee` (
`id` bigint NOT NULL,
`name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
接着是pom文件,这边看主要引入jar包,spring-boot的然后是mybatis,mybatis-spring,以及数据库连接池spring-jdbc和mysql
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置类
@MapperScan("自己的包名扫描mapper所在的包")
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setDriverClassName("com.mysql.jdbc.Driver");
//这里因为版本问题,得配后面的serverTimezone否则会报错哦
driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("123456");
return driverManagerDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
return sqlSessionFactoryBean.getObject();
}
}
mapper接口,不要在意为什么没有用对象接受啊
public interface EmployeeMapper {
@Select("select * from employee")
List<Map<String, Object>> selectAll();
}
service层
@Service
public class EmployeeService {
@Resource
private EmployeeMapper employeeMapper;
public void selectAll(){
List<Map<String, Object>> maps =
employeeMapper.selectAll();
System.out.println(maps);
}
测试类
@ComponentScan(basePackages = ""自己的包名"")
public class ExtendMybatisTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext();
applicationContext.register(ExtendMybatisTest.class);
applicationContext.refresh();
EmployeeService employeeService = (EmployeeService)applicationContext.getBean("employeeService");
employeeService.selectAll();
}
}
打印
[{name=zhangsan, id=1}]
完美运行。这边对于大家应该很熟悉并且没有任何难点那么小编要提出问题了
问题
不知道有没有和小编一样有这样的疑问,接口为什么可以注入,谁实现了接口,以及实现之后怎么被注入到spring容器中的。看注释
@Service
public class EmployeeService {
/**
* 1、EmployeeMapper注入的必须是个实例化好后的对象
* 2、这里必须实现了EmployeeMapper 接口
* 3、它必须注册到这个spring容器中
*
*
*/
@Resource
private EmployeeMapper employeeMapper;
public void selectAll(){
List<Map<String, Object>> maps =
employeeMapper.selectAll();
System.out.println(maps);
}
其实1,和2 两步骤都是mybatis完成的,到时候小编写mybatis的时候将会着重说一下,待会儿小编也会模拟一个mybatis进行说明,关键第三步被spring容器管理是怎么管理的。
首先是mybatis如何将接口变成一个可使用的对象,主要原理为动态代理。大家可以上官网查看
https://mybatis.org/mybatis-3/getting-started.html 官方start示例
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory =
new JdbcTransactionFactory();
Environment environment =
new Environment(“development”, transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(configuration);try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
这里的mapper就是从接口变成了一个实例
下面小编写一个模拟mybatis的代码的代码并且运行。
我们先编写一个生产sqlSession的类
public class ForgeMybatisSqlSessionFactory {
public static <T> T getMapper(Class<T> clazz) {
Class[] classes = new Class[]{clazz};
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//执行数据库连接,参数组装等等
System.out.println("-----data base conn and param ok------");
Select annotation = method.getAnnotation(Select.class);
String s = annotation.value()[0];
System.out.println("-----execute sql-----" + s);
return null;
}
};
T t = (T) Proxy.newProxyInstance(ForgeMybatisSqlSessionFactory.class.getClassLoader(), classes, invocationHandler);
return t;
}
}
测试类
public class ExtendMybatisTest {
public static void main(String[] args) {
EmployeeMapper employeeMapper = ForgeMybatisSqlSessionFactory.getMapper(EmployeeMapper.class);
employeeMapper.selectAll();
}
}
打印结果
-----data base conn and param ok------
-----execute sql-----select * from employee
现在我们差不多以及完成了代理类实例了那接下来怎么交由spring 管理?(扩展问题如果将第三方jar中对象或自己的对象让spring管理)
小编想到方法有以下几点:
- applicationContext.getBeanFactory().registerSingleton(“employeeMapper”,employeeMapper);
- @Bean 直接在配置文件中写入
- 使用factoryBean注入
1方案
@ComponentScan(basePackages = "自己的包")
public class ExtendMybatisTest {
public static void main(String[] args) {
//直接做一个然后放入到beanFactory中
EmployeeMapper employeeMapper = ForgeMybatisSqlSessionFactory.getMapper(EmployeeMapper.class);
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext();
applicationContext.register(ExtendMybatisTest.class);
applicationContext.getBeanFactory().registerSingleton("employeeMapper",employeeMapper);
applicationContext.refresh();
EmployeeService employeeService = (EmployeeService)applicationContext.getBean("employeeService");
employeeService.selectAll();
}
}
2 方案
@Configuration
public class DataSourceConfig {
@Bean
public EmployeeMapper employeeMapper(){
return ForgeMybatisSqlSessionFactory.getMapper(EmployeeMapper.class);
}
1和2显然不是mybatis-spring整合jar包的方案,每次都得自己加不得累死啊。
那显然是第三种方案,利用factoryBean使用,那先讲讲factoryBean,它首先得是一个bean,可以放入beanDefinitionMap中,然后必须实现一个接口,并且当前这个bean还可以产生一个bean对象。
小编先写个示例:
伪造factoryBean
@Component
public class ForgeMybatisFactoryBean implements FactoryBean {
@Override
public Object getObject() throws Exception {
return new Forge();
}
@Override
public Class<?> getObjectType() {
return Forge.class;
}
}
//没有被加入spring管理的类
public class Forge {
}
//测试
@ComponentScan(basePackages = "自己的包")
public class ExtendMybatisTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext();
applicationContext.register(ExtendMybatisTest.class);
applicationContext.refresh();
Object forgeMybatisFactory = applicationContext.getBean("forgeMybatisFactory");
System.out.println(forgeMybatisFactory);
Object forgeMybatisFactoryObject = applicationContext.getBean("&forgeMybatisFactory");
System.out.println(forgeMybatisFactoryObject);
}
}
打印结果
//这个根据名字获得的实例竟然是里面产生的object
com.dtyunxi.yundt.extend.util.Forge@61009542
//获取自己则是需要前面加&符号的,这个下次讲源码的时候小编告诉大家
com.dtyunxi.yundt.extend.util.ForgeMybatisFactory@77e9807f
示例结束,然后将上面示例中new Forge()换成ForgeMybatisSqlSessionFactory.getMapper(EmployeeMapper.class);然后下面的Forge.class换成EmployeeMapper.class,这样也就可以运行了,这样大致轮廓就出来了,然后我们看下mybatis官网 ,这里是怎么整合的
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
https://mybatis.org/spring/getting-started.html 这边主要用xml形式做的,这样就将factoryBean注入到spring容器中的,当然我们用了代码的方式并且用了@Component注解,注解这样是不可取,因为如果第三方jar包需要扫描,则在启动类或配置中放入扫描的包,这样扩展做得极差,且不易维护,而且各个咱们自己扩展的factoryBean只能实例化一个,看完xml后我们可以继续扩展,还有一个问题如何扫描多个mapper并且扫描到spring容器中。
通过以上解释大家大概明白了吧,然后小编继续改进他,改完他之后,看最开始的示例代码大家就明白了,为什么要这么写。我们慢慢扩展
首先factoryBean修改
public class ForgeMybatisFactoryBean<T> implements FactoryBean<T> {
private Class<T> mapperInterface;
public ForgeMybatisFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public void setMapperInterface(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
@Override
public T getObject() throws Exception {
return ForgeMybatisSqlSessionFactory.getMapper(mapperInterface);
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
}
将自己的factoryBean注入到spring容器,变成beanDefiniton扔进beanDefinitonMap中去
public class ForgeMybatisImportRegistry implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
/**
* 扫描bean 并注入模拟
*
*/
List<Class> classList = new ArrayList<>();
classList.add(EmployeeMapper.class);
for (Class aClass : classList) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ForgeMybatisFactoryBean.class);
AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
//属性注入方式
// MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
// propertyValues.add("mapperInterface", aClass);
//构造方法注入,需要参数的构造方法
ConstructorArgumentValues constructorArgumentValues = beanDefinition.getConstructorArgumentValues();
constructorArgumentValues.addIndexedArgumentValue(0,aClass);
registry.registerBeanDefinition(aClass.getSimpleName, beanDefinition);
}
}
}
测试类修改
@ComponentScan(basePackages = "com.dtyunxi.yundt.extend")
@Import(ForgeMybatisImportRegistry.class)
public class ExtendMybatisTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext();
applicationContext.register(ExtendMybatisTest.class);
applicationContext.refresh();
EmployeeService employeeService = (EmployeeService)applicationContext.getBean("employeeService");
employeeService.selectAll();
}
}
这样是可以顺利运行的当然还有点东西了,就是注解扫描我们来模拟一个自己的扫描类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(ForgeMybatisImportRegistry.class)
public @interface ForgeMybatisScan {
String[] basePackages() default {};
}
这样整体都差不多了,然后扫描这部我们就不做了。接下来我们看spring-mybateis源码。基本和小编编写的差不多
紧接下文
由于文章比较长大家肯定有视觉疲劳,接着看下一篇文章。
Spring与mybatis整合原理2