引言
数据存储是当今商业运行必不可少的一部分,数据库相关的开发工作更是计算机软件开发人员在日常工作中最频繁涉及的内容。数据库开发的技术经过这么多年的发展已经相当成熟,特别是应用spring+mybaies的方案后业务开发同学仅需要做很少的工作就可以正常执行数据库的增删改查,把业务同学从繁琐的数据库链接维护等工作中解放出来可以专注于业务开发。但提供方便的同时也把很多技术细节封闭起来,导致很多同学对数据库访问技术知其然不知其所以然,典型的情况就是出了问题不知从何排查、只定义了一个接口和xml配置文件为何就能查出数据了。
阅读完本文你可以掌握以下知识:
本文使用的开发环境和工具:
你可以在这里下载到调试源代码: https://github.com/hzgeyule/lean.git
另外本文假设你已经有了数据库开发的经验且用过springjpa+mybatis
Java访问数据库的方式
当前访问数据库最流行的方式是springJpa+mybaties。几年前有很多只使用springJpa的方式。更早之前就是最原始的方式,需要自己链接数据库驱动维护数据库链。最推荐的方式是第一种,在本文之所以提到后面的两种是为了追本溯源把第一种的原理讲清楚。
在介绍具体的使用方法之前先建一张表供测试使用
CREATE TABLE `t_user` ( `userid` varchar(10) NOT NULL, `username` varchar(255) DEFAULT NULL, `sex` varchar(2) DEFAULT NULL, `age` int DEFAULT NULL, PRIMARY KEY (`userid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
原始方式
最原始的数据库访问分为有几个步骤:
1、 加载数据库驱动 2、 创建并获取数据库链接 3、 创建jdbcstatement对象 4、 设置sql语句 5、 设置sql语句中的参数(使用preparedStatement) 6、 通过statement执行sql并获取结果 7、 对sql执行结果进行解析处理 8、 释放资源(resultSet、preparedstatement、connection)
代码一般如下:
try {
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 通过驱动管理类获取数据库链接
connection = DriverManager
.getConnection(
"jdbc:mysql://localhost:3306/mybatistest?characterEncoding=utf-8",
"root", "1234");
// 定义sql语句 ?表示占位符
String sql = "select * from user where username = ?";
// 获取预处理statement
preparedStatement = connection.prepareStatement(sql);
// 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
preparedStatement.setString(1, "赵六");
// 向数据库发出sql执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
// 遍历查询结果集
while (resultSet.next()) {
System.out.println(resultSet.getString("id") + " "
+ resultSet.getString("username"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
springjpa
该方式有以下几部分代码
org.springframework.boot spring-boot-starter-data-jpamysql mysql-connector-java8.0.28
spring:
datasource:
url: jdbc:mysql://localhost:3306/xxx
username: xx
password: **
driver-class-name: com.mysql.jdbc.Driver
package com.example.lean.database;
import com.example.lean.database.User;
import com.example.lean.database.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserDaoJdbcTemplateImpl implements UserDao {
//自动导入依赖的bean
@Autowired
private NamedParameterJdbcTemplate jdbcTemplate;
@Override
public int insertUser(User user) {
String sql = "INSERT INTO t_user (userid, username, sex, age) VALUES (:userid, :username, :sex, :age)";
Map param = new HashMap();
param.put("userid", user.getUserId());
param.put("username", user.getUserName());
param.put("sex", user.getSex());
param.put("age", user.getAge());
return jdbcTemplate.update(sql, param);
}
}
SpringJpa+mybaties
该方式在单纯SpringJpa的方式上格外增加几部分配置
org.mybatis.spring.boot mybatis-spring-boot-starter2.2.2
mybatis:
mapper-locations: classpath:mybatis/mapper
User queryUserByUserid(String userid);
User queryUserByUsername(String username);
}
package com.example.lean.database;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping("/get")
public Response get(@RequestParam("userId") String userId){
User user= userMapper.queryUserByUserid(userId);
return new Response(user.toString());
}
}
SpringJpa原理分析
SpringJpa的启动过程
datasource的加载过程
datasource的加载过程
org.springframework.boot.autoconfigure.EnableAutoConfiguration= org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,
所以springboot会在启动时处理这两个配置类(此处涉及springboot启动过程中的自动装配,原理可以参考百度安全验证)
进入org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration.Hikari。有对HikariDataSource类型的datasource配置
进入createDataSource方法可以看到datasource的使用的数据库链接、用户名、密码等就是我们在application.yml配置文件中的信息
至此就配置了HikariDataSource类型的DataSource,该类型目前是spring默认的DataSource因为它支持池化技术且是目前实现最好的方案。
JdbcTemplate的加载过程
JdbcTemplateAutoConfiguration中有如下配置
会加载基础的JdbcTemplate和NamedParameterJdbcTemplate
springjpa的查询原理
以NamedParameterJdbcTemplate为例,入口如下,需要明确编写调用template的update方法的代码
进入update方法逐步跟踪最终会进入JdbcTemplate的execute方法,该方法中执行的过程与传统jdbc访问数据库的流程一致。
其中第一步加载数据库驱动在springboot启动时配置datasource时已经完成了。下面分析其余各步骤在JdbcTemplate中是如何完成的
SpringJpa+mybaties的原理分析
mybatis的配置和启动过程
mybatis-spring-boot-starter的引入会引入其相关配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration= org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration, org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
同样springboot在启动时也会处理这两个配置类
在MybatisAutoConfiguration会配置SqlSessionFactory、SqlSessionTemplate(具体配置了哪些信息后文详述)、AutoConfiguredMapperScannerRegistrar这几个类,在后续的加载、查询过程中会用到
AutoConfiguredMapperScannerRegistrar实现了importBeanDefinitionRegistrar且被@import导入
springboot启动的步骤中会处理所有该种方式配置的bean(spring的启动机制):AutoConfiguredMapperScannerRegistrar会被放入MapperScannerRegistrarNotFoundConfiguration的importBeanDefinitionRegistrars中
在springboot启动中会调用importBeanDefinitionRegistrars中继承importBeanDefinitionRegistrar的重写的registerBeanDefinitions方法(springboot的启动机制)。
在AutoConfiguredMapperScannerRegistrar的该方法中在容器中加入了MapperScannerConfigurer
的beanDefinition(有一个关键属性定义为builder.addPropertyValue("annotationClass", Mapper.class))。MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor其中的postProcessBeanDefinitionRegistry方法会在启动过程中被调用(springboot的启动机制)
在该方法中定义了一个ClassPathMapperScanner(其中有个属性设置为scanner.setAnnotationClass(this.annotationClass),就会取到设置的Mapper.class
。之后调用ClassPathMapperScanner的scan方法继续调用doScan方法扫描到所有用@mapper注解的类,比如我们定义的UserMapper。
会给每个@mapper注解的类生成一个beanDefinition用来后面生成bean。这个beanDefinition的BeanClass设置成了class org.mybatis.spring.mapper.MapperFactoryBean
至此spring把我们所有用@Mapper注解的类都加载了一份BeanDefinition进spring容器。下面就要执行spring的bean生成过程(spring机制,先找到所有需要容器管理的bean的定义--beandefinition,在根据beanDefinition生成bean)。
MapperFactoryBean是一个工厂bean,在springboot生成bean的时候会调用她的getObject方法(spring机制)
getSqlSession会返回sqlSessionTemplate 这个就是在MybatisAutoConfiguration中启动时配置的。回过头来看下这个配置过程
构造方法需要SqlSessionFactory。看下SqlSessionFactory的构建过程,会拿到mybatis相关的配置,和mapper.xml文件的位置。之后调用SqlSessionFactoryBean的getObject方法
在SqlSessionFactoryBean的getObject方法中会处理所有的mapper文件,先构造一个XMLMapperBuilder,然后parse
parse后主要生成了一些MappedStatement,会把这些MappedStatement放到SqlSessionFactory的configure属性中,configure中同时有一个属性protected final MapperRegistry mapperRegistry = new MapperRegistry(this)。
继续回到实例化bean的过程
最终是使用SqlSessionFactory中的configure的getMapper方法,调用mapperRegistry.getMapper
最终返回了一个MapperProxy对象。也就是用@Mapper注解的接口最终都会返回一个MapperProxy。这个MapperProxy有一个sqlSesstion(实际类型是SqlSessionTemplate,这个SqlSessionTemplate中成员变量包含一个SqlSessionFactory,SqlSessionFactory包含MappedStatement信息)
mybatis的查询原理
通过上面的分析我们知道用@mapper注解的类最终都生成了一个MapperProxy,这一个一个代理类,实际调用时会触发她的invoke方法。最终会调用到PlainMethodInvoker的invoke方法
会交由mapperMethod.execute执行
之后交给sqlSessionTemplate执行,会根据不同的操作来选择不同的方法。
具体执行时,先找到sqlSessionTemplate中的SqlSessionFactory在找到configure信息在找到对应的MappedStatement。
之后是mybatis内部更详细的查询流程是相对独立的一部分之后会专门写文章分析。
总结
SpringJpa+mybatis的整体运行原理分成两个部分:启动时的加载配置和查询。启动加载中springJpa会根据配置的驱动和数据库链接信息把datasource和基础的JdbcTemplate加载到容器中,用户可以利用JdbcTemplate完成数据库的操作,此时用户已经不需要用关心数据链接的管理,但使用起来还稍显不便:a.sql语句杂糅在代码中 b.要显示组装查询数据库的参数、调用查询方法等。mybatis在springJpa的基础上会配置自己的sqlSessionTemplate来执行最终的查询。它同时引入了Mapper配置文件和@Mapper注解来实现sql语句与代码的分离,且不需要手动调用sqlSessionTemplate,让开发同学更专注于业务逻辑。
启动加载中核心利用了springboot的自动装配机制,以及很多spring容器的处理机制(在文中都有标注),这些机制在阅读springboot的源码中会不可避免的用到,通过学习这些机制大家可以尝试运用去阅读其他spring组件的源码比如springcache/springsecurity等。



