全部笔记All notes

Web程序设计笔记14——第十章:Spring 和 MyBatis 的整合

阅读 11m 47s11m 47s read

Spring 和 MyBatis 整合完整指南

概述

Spring 和 MyBatis 的整合是企业级应用开发中最常见的技术组合之一。通过整合,我们可以充分利用 Spring 的 IoC 容器管理 MyBatis 的核心组件,同时使用 Spring 的声明式事务管理功能。本章将详细介绍多种整合方式及其最佳实践。

💡 整合的核心价值:

  • 统一管理:由 Spring 容器管理 SqlSessionFactory、Mapper 等组件
  • 事务集成:使用 Spring 的声明式事务管理
  • 简化配置:减少 MyBatis 的配置工作
  • 提高效率:自动管理资源的创建和释放

整合方式对比

整合方式特点适用场景配置复杂度
传统 DAO继承 SqlSessionDaoSupport简单 CRUD,遗留项目高
MapperFactoryBean单个 Mapper 配置少量 Mapper中
MapperScannerConfigurer批量扫描 Mapper推荐使用低
@MapperScan注解方式扫描SpringBoot 项目最低

环境准备

Maven 依赖配置

<dependencies>
    <!-- Spring 核心依赖 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.23</version>
    </dependency>
    
    <!-- Spring JDBC 和事务支持 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.23</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>5.3.23</version>
    </dependency>
    
    <!-- MyBatis 核心 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.11</version>
    </dependency>
    
    <!-- MyBatis-Spring 整合包 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.7</version>
    </dependency>
    
    <!-- 数据库驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.31</version>
    </dependency>
    
    <!-- 连接池 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-dbcp2</artifactId>
        <version>2.9.0</version>
    </dependency>
    
    <!-- 测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.23</version>
        <scope>test</scope>
    </dependency>
</dependencies>

数据库准备

-- 创建数据库
CREATE DATABASE IF NOT EXISTS ssm_demo CHARACTER SET utf8mb4;

USE ssm_demo;

-- 创建客户表
CREATE TABLE t_customer (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL COMMENT '用户名',
    jobs VARCHAR(50) COMMENT '职业',
    phone VARCHAR(20) COMMENT '电话',
    email VARCHAR(50) COMMENT '邮箱',
    status INT DEFAULT 1 COMMENT '状态:1-正常,0-禁用',
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入测试数据
INSERT INTO t_customer (username, jobs, phone, email) VALUES 
('张三', 'Java工程师', '13800138001', 'zhangsan@email.com'),
('李四', '前端工程师', '13800138002', 'lisi@email.com'),
('王五', '测试工程师', '13800138003', 'wangwu@email.com');

传统DAO方式整合

1. 创建实体类

package com.example.domain;

import java.util.Date;

public class Customer {
    private Integer id;
    private String username;
    private String jobs;
    private String phone;
    private String email;
    private Integer status;
    private Date createTime;
    private Date updateTime;
    
    // 构造方法
    public Customer() {}
    
    public Customer(String username, String jobs, String phone) {
        this.username = username;
        this.jobs = jobs;
        this.phone = phone;
    }
    
    // Getter 和 Setter 方法
    public Integer getId() {
        return id;
    }
    
    public void setId(Integer id) {
        this.id = id;
    }
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String getJobs() {
        return jobs;
    }
    
    public void setJobs(String jobs) {
        this.jobs = jobs;
    }
    
    public String getPhone() {
        return phone;
    }
    
    public void setPhone(String phone) {
        this.phone = phone;
    }
    
    public String getEmail() {
        return email;
    }
    
    public void setEmail(String email) {
        this.email = email;
    }
    
    public Integer getStatus() {
        return status;
    }
    
    public void setStatus(Integer status) {
        this.status = status;
    }
    
    public Date getCreateTime() {
        return createTime;
    }
    
    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
    
    public Date getUpdateTime() {
        return updateTime;
    }
    
    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }
    
    @Override
    public String toString() {
        return "Customer{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", jobs='" + jobs + '\'' +
                ", phone='" + phone + '\'' +
                ", email='" + email + '\'' +
                ", status=" + status +
                '}';
    }
}

2. 创建 Mapper XML 文件

在 src/main/resources 下创建 mapper/CustomerMapper.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="CustomerMapper">
    
    <!-- 根据ID查询客户 -->
    <select id="findCustomerById" parameterType="int" 
            resultType="com.example.domain.Customer">
        SELECT * FROM t_customer WHERE id = #{id}
    </select>
    
    <!-- 查询所有客户 -->
    <select id="findAllCustomers" resultType="com.example.domain.Customer">
        SELECT * FROM t_customer ORDER BY id
    </select>
    
    <!-- 插入客户 -->
    <insert id="insertCustomer" parameterType="com.example.domain.Customer"
            useGeneratedKeys="true" keyProperty="id">
        INSERT INTO t_customer (username, jobs, phone, email)
        VALUES (#{username}, #{jobs}, #{phone}, #{email})
    </insert>
    
    <!-- 更新客户 -->
    <update id="updateCustomer" parameterType="com.example.domain.Customer">
        UPDATE t_customer
        <set>
            <if test="username != null">username = #{username},</if>
            <if test="jobs != null">jobs = #{jobs},</if>
            <if test="phone != null">phone = #{phone},</if>
            <if test="email != null">email = #{email},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        WHERE id = #{id}
    </update>
    
    <!-- 删除客户 -->
    <delete id="deleteCustomer" parameterType="int">
        DELETE FROM t_customer WHERE id = #{id}
    </delete>
    
</mapper>

3. 创建 MyBatis 配置文件

在 src/main/resources 下创建 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>
        <package name="com.example.domain"/>
    </typeAliases>
    
    <!-- 加载 Mapper 文件 -->
    <mappers>
        <mapper resource="mapper/CustomerMapper.xml"/>
    </mappers>
</configuration>

4. 创建 DAO 层接口和实现类

package com.example.dao;

import com.example.domain.Customer;
import java.util.List;

public interface CustomerDao {
    // 根据ID查询客户
    Customer findCustomerById(int id);
    
    // 查询所有客户
    List<Customer> findAllCustomers();
    
    // 插入客户
    int insertCustomer(Customer customer);
    
    // 更新客户
    int updateCustomer(Customer customer);
    
    // 删除客户
    int deleteCustomer(int id);
}
package com.example.dao.impl;

import com.example.dao.CustomerDao;
import com.example.domain.Customer;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import java.util.List;

/**
 * 基于传统 DAO 方式的实现
 * 继承 SqlSessionDaoSupport 获得 SqlSession
 */
public class CustomerDaoImpl extends SqlSessionDaoSupport implements CustomerDao {
    
    @Override
    public Customer findCustomerById(int id) {
        return getSqlSession().selectOne("CustomerMapper.findCustomerById", id);
    }
    
    @Override
    public List<Customer> findAllCustomers() {
        return getSqlSession().selectList("CustomerMapper.findAllCustomers");
    }
    
    @Override
    public int insertCustomer(Customer customer) {
        return getSqlSession().insert("CustomerMapper.insertCustomer", customer);
    }
    
    @Override
    public int updateCustomer(Customer customer) {
        return getSqlSession().update("CustomerMapper.updateCustomer", customer);
    }
    
    @Override
    public int deleteCustomer(int id) {
        return getSqlSession().delete("CustomerMapper.deleteCustomer", id);
    }
}

5. Spring 配置文件

在 src/main/resources 下创建 applicationContext.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">
    
    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/ssm_demo?useSSL=false&amp;serverTimezone=UTC"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <!-- 连接池配置 -->
        <property name="initialSize" value="5"/>
        <property name="maxTotal" value="20"/>
        <property name="maxIdle" value="10"/>
        <property name="minIdle" value="5"/>
        <property name="maxWaitMillis" value="3000"/>
    </bean>
    
    <!-- 配置 SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>
    
    <!-- 配置 DAO 实现类 -->
    <bean id="customerDao" class="com.example.dao.impl.CustomerDaoImpl">
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>
    
    <!-- 配置事务管理器 -->
    <bean id="transactionManager" 
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <!-- 开启事务注解 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    
</beans>

6. 测试代码

package com.example.test;

import com.example.dao.CustomerDao;
import com.example.domain.Customer;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.List;

public class CustomerDaoTest {
    
    private ApplicationContext context;
    private CustomerDao customerDao;
    
    @Before
    public void setUp() {
        context = new ClassPathXmlApplicationContext("applicationContext.xml");
        customerDao = (CustomerDao) context.getBean("customerDao");
    }
    
    @Test
    public void testFindCustomerById() {
        Customer customer = customerDao.findCustomerById(1);
        System.out.println("查询结果:" + customer);
    }
    
    @Test
    public void testFindAllCustomers() {
        List<Customer> customers = customerDao.findAllCustomers();
        System.out.println("客户总数:" + customers.size());
        customers.forEach(System.out::println);
    }
    
    @Test
    public void testInsertCustomer() {
        Customer customer = new Customer("赵六", "产品经理", "13800138006");
        customer.setEmail("zhaoliu@email.com");
        int result = customerDao.insertCustomer(customer);
        System.out.println("插入结果:" + result);
        System.out.println("新客户ID:" + customer.getId());
    }
    
    @Test
    public void testUpdateCustomer() {
        Customer customer = customerDao.findCustomerById(1);
        customer.setJobs("高级Java工程师");
        customer.setEmail("zhangsan_new@email.com");
        int result = customerDao.updateCustomer(customer);
        System.out.println("更新结果:" + result);
    }
    
    @Test
    public void testDeleteCustomer() {
        int result = customerDao.deleteCustomer(4);
        System.out.println("删除结果:" + result);
    }
}

Mapper接口方式整合

相比传统 DAO 方式,Mapper 接口方式更加简洁,是目前的主流方式。

方式一:基于 MapperFactoryBean

1. 创建 Mapper 接口
package com.example.mapper;

import com.example.domain.Customer;
import java.util.List;

public interface CustomerMapper {
    // 根据ID查询客户
    Customer findCustomerById(int id);
    
    // 查询所有客户
    List<Customer> findAllCustomers();
    
    // 插入客户
    int insertCustomer(Customer customer);
    
    // 更新客户
    int updateCustomer(Customer customer);
    
    // 删除客户
    int deleteCustomer(int id);
}
2. 创建 Mapper XML 文件

在 src/main/resources/com/example/mapper 下创建 CustomerMapper.xml:

⚠️ 注意: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.example.mapper.CustomerMapper">
    
    <!-- 根据ID查询客户 -->
    <select id="findCustomerById" parameterType="int" 
            resultType="com.example.domain.Customer">
        SELECT * FROM t_customer WHERE id = #{id}
    </select>
    
    <!-- 查询所有客户 -->
    <select id="findAllCustomers" resultType="com.example.domain.Customer">
        SELECT * FROM t_customer ORDER BY id
    </select>
    
    <!-- 插入客户 -->
    <insert id="insertCustomer" parameterType="com.example.domain.Customer"
            useGeneratedKeys="true" keyProperty="id">
        INSERT INTO t_customer (username, jobs, phone, email)
        VALUES (#{username}, #{jobs}, #{phone}, #{email})
    </insert>
    
    <!-- 更新客户 -->
    <update id="updateCustomer" parameterType="com.example.domain.Customer">
        UPDATE t_customer
        <set>
            <if test="username != null">username = #{username},</if>
            <if test="jobs != null">jobs = #{jobs},</if>
            <if test="phone != null">phone = #{phone},</if>
            <if test="email != null">email = #{email},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        WHERE id = #{id}
    </update>
    
    <!-- 删除客户 -->
    <delete id="deleteCustomer" parameterType="int">
        DELETE FROM t_customer WHERE id = #{id}
    </delete>
    
</mapper>
3. 修改 MyBatis 配置文件
<?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>
        <package name="com.example.domain"/>
    </typeAliases>
    
    <!-- 加载 Mapper 文件 -->
    <mappers>
        <mapper resource="com/example/mapper/CustomerMapper.xml"/>
    </mappers>
</configuration>
4. Spring 配置文件(MapperFactoryBean 方式)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">
    
    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/ssm_demo?useSSL=false&amp;serverTimezone=UTC"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <!-- 连接池配置 -->
        <property name="initialSize" value="5"/>
        <property name="maxTotal" value="20"/>
        <property name="maxIdle" value="10"/>
        <property name="minIdle" value="5"/>
        <property name="maxWaitMillis" value="3000"/>
    </bean>
    
    <!-- 配置 SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>
    
    <!-- 使用 MapperFactoryBean 配置单个 Mapper -->
    <bean id="customerMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
        <property name="mapperInterface" value="com.example.mapper.CustomerMapper"/>
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>
    
    <!-- 配置事务管理器 -->
    <bean id="transactionManager" 
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <!-- 开启事务注解 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    
</beans>
5. 测试代码
package com.example.test;

import com.example.mapper.CustomerMapper;
import com.example.domain.Customer;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MapperFactoryBeanTest {
    
    private ApplicationContext context;
    private CustomerMapper customerMapper;
    
    @Before
    public void setUp() {
        context = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 获取 Mapper 接口的代理对象
        customerMapper = (CustomerMapper) context.getBean("customerMapper");
    }
    
    @Test
    public void testFindCustomerById() {
        Customer customer = customerMapper.findCustomerById(1);
        System.out.println("查询结果:" + customer);
    }
    
    @Test
    public void testInsertCustomer() {
        Customer customer = new Customer("孙七", "架构师", "13800138007");
        customer.setEmail("sunqi@email.com");
        int result = customerMapper.insertCustomer(customer);
        System.out.println("插入结果:" + result);
        System.out.println("新客户ID:" + customer.getId());
    }
}

Mapper 接口开发规范

使用 Mapper 接口编程需要遵循以下规范:

规范项要求说明
命名一致性Mapper 接口名与 XML 文件名一致如 CustomerMapper.java 和 CustomerMapper.xml
namespaceXML 的 namespace 必须是接口的全限定名如 com.example.mapper.CustomerMapper
方法名接口方法名必须与 XML 中的 id 一致如方法 findCustomerById 对应 <select id="findCustomerById">
参数类型方法参数类型与 parameterType 一致支持基本类型、包装类、JavaBean、Map 等
返回类型方法返回类型与 resultType/resultMap 对应单个对象、List、Map 等

💡 提示:遵循这些规范后,MyBatis 会自动生成 Mapper 接口的代理实现类,无需手动编写实现代码。

MapperScannerConfigurer整合

MapperScannerConfigurer 可以批量扫描 Mapper 接口,是最推荐的整合方式。

方式二:基于 MapperScannerConfigurer

1. Spring 配置文件(MapperScannerConfigurer 方式)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">
    
    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/ssm_demo?useSSL=false&amp;serverTimezone=UTC"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <!-- 连接池配置 -->
        <property name="initialSize" value="5"/>
        <property name="maxTotal" value="20"/>
        <property name="maxIdle" value="10"/>
        <property name="minIdle" value="5"/>
        <property name="maxWaitMillis" value="3000"/>
    </bean>
    
    <!-- 配置 SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!-- 可选:配置 Mapper XML 文件位置 -->
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>
    
    <!-- 使用 MapperScannerConfigurer 批量扫描 Mapper 接口 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 指定要扫描的包路径 -->
        <property name="basePackage" value="com.example.mapper"/>
        <!-- 可选:指定 SqlSessionFactory 的 bean 名称 -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>
    
    <!-- 配置事务管理器 -->
    <bean id="transactionManager" 
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <!-- 开启事务注解 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    
    <!-- 扫描 Service 层组件 -->
    <context:component-scan base-package="com.example.service"/>
    
</beans>
2. 创建 Service 层
package com.example.service;

import com.example.domain.Customer;
import java.util.List;

public interface CustomerService {
    Customer getCustomerById(int id);
    List<Customer> getAllCustomers();
    boolean addCustomer(Customer customer);
    boolean updateCustomer(Customer customer);
    boolean deleteCustomer(int id);
}
package com.example.service.impl;

import com.example.domain.Customer;
import com.example.mapper.CustomerMapper;
import com.example.service.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional
public class CustomerServiceImpl implements CustomerService {
    
    @Autowired
    private CustomerMapper customerMapper;
    
    @Override
    @Transactional(readOnly = true)
    public Customer getCustomerById(int id) {
        return customerMapper.findCustomerById(id);
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<Customer> getAllCustomers() {
        return customerMapper.findAllCustomers();
    }
    
    @Override
    public boolean addCustomer(Customer customer) {
        return customerMapper.insertCustomer(customer) > 0;
    }
    
    @Override
    public boolean updateCustomer(Customer customer) {
        return customerMapper.updateCustomer(customer) > 0;
    }
    
    @Override
    public boolean deleteCustomer(int id) {
        return customerMapper.deleteCustomer(id) > 0;
    }
}
3. 测试代码
package com.example.test;

import com.example.domain.Customer;
import com.example.mapper.CustomerMapper;
import com.example.service.CustomerService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class MapperScannerTest {
    
    @Autowired
    private CustomerMapper customerMapper;
    
    @Autowired
    private CustomerService customerService;
    
    @Test
    public void testMapperScan() {
        // 直接注入 Mapper 接口
        Customer customer = customerMapper.findCustomerById(1);
        System.out.println("Mapper查询结果:" + customer);
    }
    
    @Test
    public void testServiceLayer() {
        // 通过 Service 层访问
        List<Customer> customers = customerService.getAllCustomers();
        System.out.println("Service查询结果:");
        customers.forEach(System.out::println);
    }
    
    @Test
    public void testTransaction() {
        Customer customer = new Customer("测试事务", "测试职位", "13800138008");
        customer.setEmail("test@email.com");
        
        boolean result = customerService.addCustomer(customer);
        System.out.println("添加结果:" + result);
        System.out.println("新客户ID:" + customer.getId());
    }
}

SpringBoot整合MyBatis

SpringBoot 通过自动配置极大简化了 MyBatis 的整合过程。

1. Maven 依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.5</version>
</parent>

<dependencies>
    <!-- SpringBoot Web Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- MyBatis SpringBoot Starter -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- 数据源(使用 HikariCP) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
    <!-- 测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2. application.yml 配置

spring:
  # 数据源配置
  datasource:
    url: jdbc:mysql://localhost:3306/ssm_demo?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    # HikariCP 连接池配置
    hikari:
      minimum-idle: 5
      maximum-pool-size: 20
      idle-timeout: 600000
      max-lifetime: 1800000
      connection-timeout: 30000

# MyBatis 配置
mybatis:
  # 配置文件位置
  config-location: classpath:mybatis-config.xml
  # Mapper XML 文件位置
  mapper-locations: classpath:mapper/*.xml
  # 类型别名包路径
  type-aliases-package: com.example.domain
  # 配置项
  configuration:
    # 开启驼峰命名转换
    map-underscore-to-camel-case: true
    # 开启延迟加载
    lazy-loading-enabled: true
    # 关闭积极加载
    aggressive-lazy-loading: false
    # 开启二级缓存
    cache-enabled: true

# 日志配置
logging:
  level:
    com.example.mapper: debug

3. 主启动类

package com.example;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@MapperScan("com.example.mapper")  // 扫描 Mapper 接口
@EnableTransactionManagement        // 开启事务管理
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

4. Mapper 接口(注解方式)

package com.example.mapper;

import com.example.domain.Customer;
import org.apache.ibatis.annotations.*;
import java.util.List;

@Mapper  // 标识为 Mapper 接口
public interface CustomerMapper {
    
    // 使用注解方式编写 SQL
    @Select("SELECT * FROM t_customer WHERE id = #{id}")
    Customer findCustomerById(int id);
    
    @Select("SELECT * FROM t_customer ORDER BY id")
    List<Customer> findAllCustomers();
    
    @Insert("INSERT INTO t_customer(username, jobs, phone, email) " +
            "VALUES(#{username}, #{jobs}, #{phone}, #{email})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insertCustomer(Customer customer);
    
    @Update("UPDATE t_customer SET username=#{username}, jobs=#{jobs}, " +
            "phone=#{phone}, email=#{email} WHERE id=#{id}")
    int updateCustomer(Customer customer);
    
    @Delete("DELETE FROM t_customer WHERE id = #{id}")
    int deleteCustomer(int id);
    
    // 复杂查询使用 @Select + @Results
    @Select("SELECT * FROM t_customer WHERE jobs = #{jobs}")
    @Results({
        @Result(property = "id", column = "id"),
        @Result(property = "username", column = "username"),
        @Result(property = "jobs", column = "jobs"),
        @Result(property = "phone", column = "phone"),
        @Result(property = "email", column = "email")
    })
    List<Customer> findCustomersByJob(String jobs);
}

5. Controller 层

package com.example.controller;

import com.example.domain.Customer;
import com.example.service.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/customers")
public class CustomerController {
    
    @Autowired
    private CustomerService customerService;
    
    @GetMapping("/{id}")
    public Customer getCustomer(@PathVariable int id) {
        return customerService.getCustomerById(id);
    }
    
    @GetMapping
    public List<Customer> getAllCustomers() {
        return customerService.getAllCustomers();
    }
    
    @PostMapping
    public String addCustomer(@RequestBody Customer customer) {
        boolean result = customerService.addCustomer(customer);
        return result ? "添加成功" : "添加失败";
    }
    
    @PutMapping("/{id}")
    public String updateCustomer(@PathVariable int id, 
                               @RequestBody Customer customer) {
        customer.setId(id);
        boolean result = customerService.updateCustomer(customer);
        return result ? "更新成功" : "更新失败";
    }
    
    @DeleteMapping("/{id}")
    public String deleteCustomer(@PathVariable int id) {
        boolean result = customerService.deleteCustomer(id);
        return result ? "删除成功" : "删除失败";
    }
}

事务管理

1. 声明式事务配置

@Service
@Transactional  // 类级别事务
public class CustomerServiceImpl implements CustomerService {
    
    @Autowired
    private CustomerMapper customerMapper;
    
    // 只读事务,优化性能
    @Transactional(readOnly = true)
    public Customer getCustomerById(int id) {
        return customerMapper.findCustomerById(id);
    }
    
    // 需要事务,默认传播行为
    @Transactional
    public boolean addCustomer(Customer customer) {
        return customerMapper.insertCustomer(customer) > 0;
    }
    
    // 设置事务超时和回滚规则
    @Transactional(
        timeout = 30,
        rollbackFor = Exception.class,
        noRollbackFor = BusinessException.class
    )
    public boolean updateCustomer(Customer customer) {
        return customerMapper.updateCustomer(customer) > 0;
    }
    
    // 使用新事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void auditLog(String operation) {
        // 记录审计日志,独立事务
    }
}

2. 编程式事务

@Service
public class TransactionService {
    
    @Autowired
    private TransactionTemplate transactionTemplate;
    
    @Autowired
    private CustomerMapper customerMapper;
    
    public void batchInsert(List<Customer> customers) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                    for (Customer customer : customers) {
                        customerMapper.insertCustomer(customer);
                    }
                } catch (Exception e) {
                    status.setRollbackOnly();
                    throw e;
                }
            }
        });
    }
}

性能优化

1. 连接池优化

spring:
  datasource:
    hikari:
      # 最小空闲连接数
      minimum-idle: 10
      # 最大连接数
      maximum-pool-size: 50
      # 连接测试查询
      connection-test-query: SELECT 1
      # 连接池名称
      pool-name: SSM-HikariCP
      # 连接泄露检测阈值
      leak-detection-threshold: 60000

2. MyBatis 缓存配置

<!-- 在 Mapper XML 中配置二级缓存 -->
<mapper namespace="com.example.mapper.CustomerMapper">
    <cache 
        eviction="LRU"
        flushInterval="60000"
        size="1024"
        readOnly="true"/>
    
    <!-- 禁用特定查询的缓存 -->
    <select id="getRealTimeData" useCache="false">
        SELECT * FROM real_time_table
    </select>
</mapper>

3. 批量操作优化

@Mapper
public interface CustomerMapper {
    
    // 批量插入
    @Insert({
        "<script>",
        "INSERT INTO t_customer(username, jobs, phone, email) VALUES",
        "<foreach collection='list' item='item' separator=','>",
        "(#{item.username}, #{item.jobs}, #{item.phone}, #{item.email})",
        "</foreach>",
        "</script>"
    })
    int batchInsert(List<Customer> customers);
    
    // 批量更新
    @Update({
        "<script>",
        "<foreach collection='list' item='item' separator=';'>",
        "UPDATE t_customer SET",
        "username = #{item.username},",
        "jobs = #{item.jobs}",
        "WHERE id = #{item.id}",
        "</foreach>",
        "</script>"
    })
    int batchUpdate(List<Customer> customers);
}

最佳实践

1. 项目结构规范

src/main/java/
└── com.example/
    ├── domain/          # 实体类
    ├── mapper/          # Mapper接口
    ├── service/         # Service接口
    │   └── impl/        # Service实现
    ├── controller/      # Controller层
    ├── config/          # 配置类
    ├── exception/       # 异常处理
    └── util/           # 工具类

src/main/resources/
├── mapper/             # Mapper XML文件
├── static/             # 静态资源
├── templates/          # 模板文件
├── application.yml     # 应用配置
└── mybatis-config.xml  # MyBatis配置

2. 代码规范

// 1. Mapper 接口规范
@Mapper
public interface BaseMapper<T> {
    T selectById(Long id);
    List<T> selectAll();
    int insert(T entity);
    int update(T entity);
    int deleteById(Long id);
}

// 2. Service 层规范
@Service
@Transactional(rollbackFor = Exception.class)
public abstract class BaseService<T> {
    protected abstract BaseMapper<T> getMapper();
    
    @Transactional(readOnly = true)
    public T getById(Long id) {
        return getMapper().selectById(id);
    }
}

// 3. 统一返回结果
@Data
public class Result<T> {
    private int code;
    private String message;
    private T data;
    
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("success");
        result.setData(data);
        return result;
    }
}

3. 异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(DataAccessException.class)
    public Result<String> handleDataAccessException(DataAccessException e) {
        log.error("数据库操作异常", e);
        return Result.error("数据库操作失败");
    }
    
    @ExceptionHandler(DuplicateKeyException.class)
    public Result<String> handleDuplicateKeyException(DuplicateKeyException e) {
        return Result.error("数据已存在,请勿重复添加");
    }
}

常见问题

问题 1:找不到 Mapper Bean

原因:

  • 未配置 @MapperScan 或 MapperScannerConfigurer
  • Mapper 接口路径配置错误
  • 缺少 @Mapper 注解

解决方案:

// 方案1:启动类添加 @MapperScan
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application { }

// 方案2:Mapper 接口添加 @Mapper
@Mapper
public interface CustomerMapper { }

// 方案3:配置类中配置
@Configuration
public class MyBatisConfig {
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer configurer = new MapperScannerConfigurer();
        configurer.setBasePackage("com.example.mapper");
        return configurer;
    }
}

问题 2:事务不生效

原因:

  • 方法不是 public
  • 自调用问题
  • 异常被捕获
  • 未配置事务管理器

解决方案:

// 确保方法是 public
@Transactional
public void doSomething() { } // 正确

// 避免自调用
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
    
    public void method1() {
        serviceB.method2(); // 通过代理调用
    }
}

// 正确抛出异常
@Transactional
public void method() {
    try {
        // 业务逻辑
    } catch (Exception e) {
        throw new RuntimeException(e); // 确保异常抛出
    }
}

问题 3:MyBatis 类型处理器问题

解决方案:

// 自定义类型处理器
@MappedTypes(JsonNode.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonTypeHandler extends BaseTypeHandler<JsonNode> {
    
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                  JsonNode parameter, JdbcType jdbcType) 
                                  throws SQLException {
        ps.setString(i, parameter.toString());
    }
    
    @Override
    public JsonNode getNullableResult(ResultSet rs, String columnName) 
                                    throws SQLException {
        String json = rs.getString(columnName);
        return parseJson(json);
    }
}

// 配置类型处理器
mybatis:
  type-handlers-package: com.example.handler

问题 4:分页查询

使用 PageHelper:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.5</version>
</dependency>
@Service
public class CustomerService {
    
    public PageInfo<Customer> getCustomerPage(int pageNum, int pageSize) {
        // 设置分页参数
        PageHelper.startPage(pageNum, pageSize);
        // 执行查询
        List<Customer> customers = customerMapper.findAllCustomers();
        // 包装成分页结果
        return new PageInfo<>(customers);
    }
}

相关文章

前后章节导航

Spring 框架核心

MyBatis 核心技术

SSM 整合实践

SpringBoot 进阶

整合方式详解

1. 传统 DAO 模式
  • 使用 SqlSessionDaoSupport
  • 适合简单的 CRUD 操作
  • 需要手动管理 SqlSession
2. Mapper 接口模式
  • MapperFactoryBean:单个 Mapper 配置
  • MapperScannerConfigurer:批量扫描 Mapper
  • 推荐使用,更加简洁高效
3. 注解配置方式
  • @MapperScan 注解扫描
  • @Mapper 标识接口
  • SpringBoot 中的主流方式

常见问题与解决

  1. 配置文件加载问题

    • 检查 mapper 路径配置
    • 确保 XML 文件在 classpath 下
    • 注意包名和路径的对应关系
  2. 事务管理配置

    • 使用 Spring 的事务管理器
    • @Transactional 注解的正确使用
    • 事务传播行为的设置
  3. 数据源配置

    • 选择合适的连接池(DBCP、HikariCP、Druid)
    • 连接池参数优化
    • 数据库连接泄漏问题

最佳实践建议

  1. 代码组织结构

    • Mapper 接口与 XML 同包名
    • 统一的命名规范
    • 合理的分层设计
  2. 性能优化

    • 使用 MyBatis 二级缓存
    • 合理使用延迟加载
    • SQL 语句优化
  3. 开发效率

    • 使用 MyBatis Generator
    • 集成 MyBatis-Plus
    • 热部署配置

扩展阅读

  • MyBatis-Spring 官方文档
  • Spring Boot 自动配置原理
  • MyBatis-Plus 快速入门