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 that DataSource as an input using the SqlSessionFactoryBean
  • Will create and register an instance of a SqlSessionTemplate got out of the SqlSessionFactory
  • 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.演示环境

  1. JDK 1.8.0_201
  2. Spring Boot 2.2.0.RELEASE
  3. 构建工具(apache maven 3.6.3)
  4. 开发工具(IntelliJ IDEA )

3.演示代码

3.1 代码说明

spring boot 集成 mybatis,使用了 xml 和注解 两种配置方式。实现了单表的增删改查。

3.2 代码结构

image-20200723214856482

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

image-20200721210408036

根据用户名查询

### GET /user/findByName/{name}
GET http://localhost:8080/user/findByName/lisi
Accept: application/json

image-20200721210526269

根据手机号查询

### GET /user/findByPhone/{phone}
GET http://localhost:8080/user/findByPhone/13666666666
Accept: application/json

image-20200721210624524

添加用户

### 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"
}

image-20200726164659485

更新用户信息

### PUT /user/updateByName
PUT http://localhost:8080/user/updateByName
Content-Type: application/json

{
  "name": "zhangsan",
  "phone": "13456789012"
}

image-20200721210849107

删除用户

### DELETE /user/deleteByName/{name}
DELETE http://localhost:8080/user/deleteByName/zhangsan
Content-Type: application/json

image-20200721210950916

5.源码分析

以 UserServiceImpl#findUserByName 为例,分析一下相关源码。

5.1 mybatis 的加载

image-20200726184744481

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 的动态代理

image-20200726190240145

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查询执行流程

image-20200726193730269

由于 UserMapper 实际上是一个 MapperProxy 的代理对象,所以 UserServiceImpl 在调用 UserMapper 中方法时,实际调用到 MapperProxy 中的 invoke 方法。

image-20200726213127704

@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 方法完成查询。

6.参考

  1. Mybatis官网
原文地址:https://www.cnblogs.com/col-smile/p/13382630.html