12.SpringBoot学习(十二)——JDBC之 Spring Boot Mybatis
1.简介
1.1 概述
The MyBatis-Spring-Boot-Starter help you build quickly MyBatis applications on top of the Spring Boot.
By using this module you will achieve:
- Build standalone applications
- Reduce the boilerplate to almost zero
- Less XML configuration
MyBatis-Spring-Boot-Starter可帮助您在Spring Boot之上快速构建MyBatis应用程序。
通过使用此模块,您将实现:
- 构建独立的应用程序
- 将样板减少到几乎为零
- 减少XML配置
1.2 特点
As you may already know, to use MyBatis with Spring you need at least an
SqlSessionFactory
and at least one mapper interface.MyBatis-Spring-Boot-Starter will:
- Autodetect an existing
DataSource
- Will create and register an instance of a
SqlSessionFactory
passing thatDataSource
as an input using theSqlSessionFactoryBean
- Will create and register an instance of a
SqlSessionTemplate
got out of theSqlSessionFactory
- Auto-scan your mappers, link them to the
SqlSessionTemplate
and register them to Spring context so they can be injected into your beans
MyBatis-Spring-Boot-Starter 将会:
- 自动检测已有的
DataSource
- 将创建并注册一个
SqlSessionFactory
实例,并使用SqlSessionFactoryBean
将该DataSource
作为输入 - 将创建并注册一个从
SqlSessionFactory
中获取的SqlSessionTemplate
实例 - 自动扫描您的映射器,将它们链接到
SqlSessionTemplate
并将它们注册到 Spring 上下文中,以便可以将它们注入到您的 bean 中
2.演示环境
- JDK 1.8.0_201
- Spring Boot 2.2.0.RELEASE
- 构建工具(apache maven 3.6.3)
- 开发工具(IntelliJ IDEA )
3.演示代码
3.1 代码说明
spring boot 集成 mybatis,使用了 xml 和注解 两种配置方式。实现了单表的增删改查。
3.2 代码结构
3.3 maven 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
3.4 配置文件
application.properties
mybatis.config-location=classpath:mybatis/mybatis-config.xml
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
mybatis.type-aliases-package=com.soulballad.usage.springboot
spring.datasource.url=jdbc:mysql://172.16.11.125:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
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>
<typeAliases>
<typeAlias alias="Integer" type="java.lang.Integer"/>
<typeAlias alias="Long" type="java.lang.Long"/>
<typeAlias alias="HashMap" type="java.util.HashMap"/>
<typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap"/>
<typeAlias alias="ArrayList" type="java.util.ArrayList"/>
<typeAlias alias="LinkedList" type="java.util.LinkedList"/>
</typeAliases>
</configuration>
UserMapper.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" >
<mapper namespace="com.soulballad.usage.springboot.mapper.UserMapper">
<resultMap id="baseResultMap" type="com.soulballad.usage.springboot.model.UserModel">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="age" property="age" javaType="INTEGER"/>
<result column="birthday" property="birthday" jdbcType="VARCHAR"/>
<result column="address" property="address" jdbcType="VARCHAR"/>
<result column="phone" property="phone" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
id, `name`, age, birthday, address, phone
</sql>
<select id="findUserByName" parameterType="java.lang.String" resultMap="baseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM t_user
WHERE `name` like concat('%', #{name}, '%')
</select>
<select id="findUserByPhone" parameterType="java.lang.String" resultMap="baseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM t_user
WHERE phone = ${phone}
</select>
<update id="updateByName" parameterType="com.soulballad.usage.springboot.model.UserModel">
UPDATE t_user SET phone = #{phone} WHERE `name` = #{name}
</update>
<delete id="deleteByName" parameterType="java.lang.String">
DELETE FROM t_user WHERE `name` = #{name}
</delete>
</mapper>
3.5 java代码
UserModel.java
public class UserModel implements Serializable {
private Long id;
private String name;
private Integer age;
private String birthday;
private String address;
private String phone;
public UserModel() {}
public UserModel(String name, Integer age, String birthday, String address, String phone) {
this.name = name;
this.age = age;
this.birthday = birthday;
this.address = address;
this.phone = phone;
}
// get&set&toString
}
UserMapper.java
@Repository
public interface UserMapper {
@Select("SELECT id, `name`, age, birthday, address, phone FROM t_user")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "name", column = "name"),
@Result(property = "age", column = "age"),
@Result(property = "birthday", column = "birthday"),
@Result(property = "address", column = "address"),
@Result(property = "phone", column = "phone")
})
List<UserModel> findAll();
UserModel findUserByName(String name);
// Page<UserModel> findByPage(SpringDataWebProperties.Pageable pageable);
List<UserModel> findUserByPhone(@Param("phone") String phone);
int updateByName(UserModel userModel);
int deleteByName(@Param("name") String name);
@Insert("INSERT INTO t_user(`name`, age, birthday, address, phone) VALUES(#{name}, #{age}, #{birthday}, #{address}, #{phone})")
int insertUser(UserModel userModel);
}
UserService.java
public interface UserService {
/**
* 查询所有数据
* @return user
*/
List<UserModel> selectList();
/**
* 根据名称查询
* @param name name
* @return user
*/
UserModel findUserByName(String name);
/**
* 根据电话查询
* @param phone 电话
* @return user
*/
List<UserModel> findUserByPhone(String phone);
/**
* 根据名称更新电话
* @param phone 电话
* @param name 名称
* @return 影响行数
*/
UserModel updateByName(String phone, String name);
/**
* 根据名称删除
* @param name 名称
* @return 影响行数
*/
UserModel deleteByName(String name);
/**
* 新增
* @param user user
* @return user
*/
UserModel add(UserModel user);
}
UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public List<UserModel> selectList() {
return userMapper.findAll();
}
@Override
public UserModel findUserByName(String name) {
return userMapper.findUserByName(name);
}
@Override
public List<UserModel> findUserByPhone(String phone) {
return userMapper.findUserByPhone(phone);
}
@Override
public UserModel updateByName(String phone, String name) {
UserModel userModel = new UserModel();
userModel.setPhone(phone);
userModel.setName(name);
userMapper.updateByName(userModel);
return findUserByName(name);
}
@Override
public UserModel deleteByName(String name) {
UserModel user = findUserByName(name);
userMapper.deleteByName(name);
return user;
}
@Override
public UserModel add(UserModel user) {
userMapper.insertUser(user);
return findUserByName(user.getName());
}
}
UserController.java
@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping(value = "/list")
public List<UserModel> list() {
return userService.selectList();
}
@GetMapping(value = "/findByName/{name}")
public UserModel findByName(@PathVariable String name) {
return userService.findUserByName(name);
}
@GetMapping(value = "/findByPhone/{phone}")
public List<UserModel> findByPhone(@PathVariable String phone) {
return userService.findUserByPhone(phone);
}
@PostMapping(value = "/add")
public UserModel add(@RequestBody UserModel user) {
return userService.add(user);
}
@PutMapping(value = "/updateByName")
public UserModel updateByName(@RequestBody UserModel user) {
return userService.updateByName(user.getPhone(), user.getName());
}
@DeleteMapping(value = "/deleteByName/{name}")
public UserModel deleteByName(@PathVariable String name) {
return userService.deleteByName(name);
}
}
3.6 git 地址
spring-boot/spring-boot-06-jdbc/spring-boot-mybatis
4.效果展示
启动 SpringBootMybatisApplication.main 方法,在 spring-boot-mybatis.http 访问下列地址,观察输出信息是否符合预期。
查询用户列表(所有)
### GET /user/list
GET http://localhost:8080/user/list
Accept: application/json
根据用户名查询
### GET /user/findByName/{name}
GET http://localhost:8080/user/findByName/lisi
Accept: application/json
根据手机号查询
### GET /user/findByPhone/{phone}
GET http://localhost:8080/user/findByPhone/13666666666
Accept: application/json
添加用户
### POST user/add
POST http://localhost:8080/user/add
Content-Type: application/json
{
"name": "abcd12",
"age": 33,
"birthday": "1987-07-20",
"address": "washington",
"phone": "15666666666"
}
更新用户信息
### PUT /user/updateByName
PUT http://localhost:8080/user/updateByName
Content-Type: application/json
{
"name": "zhangsan",
"phone": "13456789012"
}
删除用户
### DELETE /user/deleteByName/{name}
DELETE http://localhost:8080/user/deleteByName/zhangsan
Content-Type: application/json
5.源码分析
以 UserServiceImpl#findUserByName 为例,分析一下相关源码。
5.1 mybatis 的加载
SqlSessionFactoryBean 实现了 InitializingBean 接口,重写了 afterPropertiesSet 方法,在这个方法中对 sqlSessionFactory 进行了初始化
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = buildSqlSessionFactory();
}
在 buildSqlSessionFactory 中解析 config 和 mapper 配置文件、扫描注解,将所有配置信息保存到 configuration 中。
// 解析 mybatis-config.xml
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
// 解析 properties 节点
propertiesElement(root.evalNode("properties"));
// 解析 settings 节点
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 解析 typeAliases 节点
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 plugins 节点
pluginElement(root.evalNode("plugins"));
// 解析 objectFactory 节点
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 objectWrapperFactory 节点
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 reflectorFactory 节点
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
// 解析 UserMapper.xml
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 解析 mapper 节点
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 生成 MappedStatement 对象,保存到 configuration 中
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
5.2 UserMapper 的动态代理
MapperFactoryBean 是 UserMapper 实例化是对应的 beanClass,它继承自 DaoSupport 间接实现了 InitializingBean 接口,同时自己实现了 FactoryBean 接口,在 UserMapper 实例化时,会调用它的 getObject 方法,按照自定义逻辑创建对象。
在 getObject 中,它要返回一个 Mapper 对象
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
这里的 getSqlSession 实际上调用父类 SqlSessionDaoSupport 的方法,返回一个 SqlSessionTemplate。它是 SqlSession 的一个实现类,重写了 SqlSession 中一些操作数据库的常用方法,相当于一个工具类。
@Override
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
getMapper 是从解析配置文件后保存的 mapperRegistry 中获取一个 mapper 对象
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// MapperProxyFactory 工厂类
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
它通过 MapperProxyFactory 工厂类来创建具体的对象,创建时,最终生成为 Mapper 的代理对象
protected T newInstance(MapperProxy<T> mapperProxy) {
// jdk 动态代理
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
5.3 mybatis查询执行流程
由于 UserMapper 实际上是一个 MapperProxy 的代理对象,所以 UserServiceImpl 在调用 UserMapper 中方法时,实际调用到 MapperProxy 中的 invoke 方法。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 返回一个 PlainMethodInvoker 对象,并调用 invoke 方法
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
PlainMethodInvoker 是 MapperMethodInvoker 的一个实现,它的 invoke 方法如下
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
最终调用 sqlSession.selectOne 方法,这里的 sqlSession 是 SqlSessionTemplate
@Override
public <T> T selectOne(String statement, Object parameter) {
return this.sqlSessionProxy.selectOne(statement, parameter);
}
而 sqlSessionProxy 是 sqlSession 的一个代理对象,它在 SqlSessionTemplate 构造函数中创建
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// jdk 代理,代理对象是 SqlSessionInterceptor,被代理对象时 SqlSessionFactory 的实例
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
所以执行 SqlSessionInterceptor 中的 invoke 方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取 sqlSession,返回一个 DefaultSqlSession
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
// 调用 selectOne 方法
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
通过反射调用 selectOne 方法,目标对象是 DefaultSqlSession,
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// MappedStatement 中封装了每个 sql 操作中各项参数
MappedStatement ms = configuration.getMappedStatement(statement);
// 使用 executor 进行查询
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
这里的 executor 获取 DefaultSqlSession 时创建
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// 执行器类型,默认是 Simple
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
// 创建一个 SimpleExecutor
executor = new SimpleExecutor(this, transaction);
}
// cacheEnabled 默认为 true
if (cacheEnabled) {
// 使用 CachingExecutor 对 SimpleExecutor 进行装饰
executor = new CachingExecutor(executor);
}
// 这里使用插件对 executor 进行代理,生成拦截器链。当前没配置插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
获取到的 executor 类型为 CachingExecutor,它装饰了 SimpleExecutor,SimpleExecutor 继承于 BaseExecutor。
所以执行查询时先调用 CachingExecutor#query,调用 delegate.query 时,又调用 BaseExecutor#query,这个 query 方法为模板 方法,最终调用到 SimpleExecutor#queryFromDatabase。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 执行查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
在 doQuery 方法中,调用 handler.query
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 创建 handler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 构建 statement,包括获取 connection、设置参数
stmt = prepareStatement(handler, ms.getStatementLog());
// 使用 handler 查询
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
这里的 handler 是被 RoutingStatementHandler 装饰的 PreparedStatementHandler
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 执行sql
ps.execute();
// 处理结果集
return resultSetHandler.handleResultSets(ps);
}
最终调用 PreparedStatement 的 execute 方法完成查询。