1. 为什么选择Spring整合MyBatis?
在Java企业级应用开发中,持久层框架的选择往往决定了项目的开发效率和后期维护成本。MyBatis作为半自动化的ORM框架,与Spring生态的整合已经成为现代Java开发的标配组合。这种组合方式完美结合了Spring的依赖注入优势和MyBatis的SQL灵活性。
我经历过多个从Hibernate迁移到MyBatis的项目,最大的感受是:对于复杂业务系统,特别是需要精细控制SQL性能的场景,MyBatis提供的原生SQL控制能力是无可替代的。Spring的IoC容器则让MyBatis的配置和使用变得更加优雅。
注意:虽然JPA规范日趋完善,但在需要复杂查询、存储过程调用或数据库特定功能时,MyBatis仍然是更灵活的选择。
1.1 技术选型对比
让我们先看一个简单的对比表格,了解不同持久层方案的特点:
| 特性 | 纯JDBC | Hibernate | MyBatis | Spring Data JPA |
|---|---|---|---|---|
| SQL控制度 | 完全控制 | 几乎无控制 | 完全控制 | 有限控制 |
| 学习曲线 | 陡峭 | 中等 | 平缓 | 平缓 |
| 性能优化空间 | 极大 | 有限 | 极大 | 有限 |
| 与Spring集成难度 | 高 | 低 | 低 | 极低 |
| 适合场景 | 遗留系统 | CRUD密集型 | 复杂查询 | 快速开发 |
从实际项目经验来看,MyBatis特别适合以下场景:
- 需要复用现有复杂SQL的场景
- 需要调用存储过程的项目
- 对性能有极致要求的核心业务模块
- 数据库设计不符合JPA规范的遗留系统
1.2 版本兼容性考量
在开始整合前,版本匹配是首要考虑的问题。根据我的踩坑经验,不同版本的组合可能会带来意想不到的问题:
- Spring 5.x + MyBatis 3.5+ 是最稳定的组合
- 如果使用Spring Boot,2.7.x版本对MyBatis的支持最成熟
- 要特别注意MyBatis-Spring桥接包的版本,它必须与MyBatis核心版本匹配
我曾经在一个项目中使用Spring Boot 2.4 + MyBatis 3.4.6,结果遇到了事务管理不生效的问题。后来发现是桥接包版本不兼容导致的。这个教训告诉我:版本匹配表必须作为项目启动时的第一份技术文档。
2. 环境准备与基础配置
2.1 Maven依赖配置
对于Maven项目,pom.xml中需要配置以下核心依赖:
<!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.7.12</version> </dependency> <!-- MyBatis核心 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <!-- MyBatis-Spring桥接 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <!-- Spring JDBC (事务管理需要) --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.23</version> </dependency> <!-- 数据库驱动 (以MySQL为例) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency>在实际项目中,我通常会额外添加这些实用依赖:
- MyBatis Plus(增强工具包)
- PageHelper(分页插件)
- Druid(高性能连接池)
2.2 数据源配置
Spring Boot的application.yml中配置数据源:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=UTC username: root password: 123456 type: com.alibaba.druid.pool.DruidDataSource # Druid连接池专用配置 druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 FROM DUAL test-while-idle: true test-on-borrow: false test-on-return: false经验分享:Druid的连接池监控页面非常实用,可以通过以下配置开启:
spring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin
2.3 MyBatis配置类
创建MyBatis的Spring配置类:
@Configuration @MapperScan("com.example.mapper") // 指定Mapper接口所在包 public class MyBatisConfig { @Autowired private DataSource dataSource; @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); // 配置XML文件位置 factoryBean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml")); // 添加自定义配置 org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); // 开启驼峰命名转换 configuration.setDefaultFetchSize(100); // 设置默认获取数量 configuration.setDefaultStatementTimeout(30); // 设置超时时间 factoryBean.setConfiguration(configuration); return factoryBean.getObject(); } @Bean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } }我曾经在一个项目中因为没有设置defaultStatementTimeout,导致某些复杂查询长时间运行却不超时,最终拖垮了整个数据库。这个配置项对于生产环境至关重要。
3. Mapper开发全流程
3.1 接口与XML的对应关系
MyBatis的核心设计理念就是将Java接口与XML映射文件绑定。这种设计带来了极大的灵活性,但也容易因配置不当导致各种问题。
正确的对应关系应该遵循这些规则:
- 接口全限定名与XML的namespace完全一致
- 方法名与XML中的id属性一致
- 参数类型与parameterType匹配
- 返回类型与resultType/resultMap匹配
一个典型的Mapper接口示例:
public interface UserMapper { @Select("SELECT * FROM users WHERE id = #{id}") User selectById(@Param("id") Long id); List<User> selectByCondition(UserQuery query); @Options(useGeneratedKeys = true, keyProperty = "id") @Insert("INSERT INTO users(name,email) VALUES(#{name},#{email})") int insert(User user); @Update("UPDATE users SET name=#{name}, email=#{email} WHERE id=#{id}") int update(User user); @Delete("DELETE FROM users WHERE id=#{id}") int delete(Long id); }对应的XML映射文件(resources/mapper/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.example.mapper.UserMapper"> <resultMap id="userResultMap" type="com.example.entity.User"> <id property="id" column="id"/> <result property="name" column="name"/> <result property="email" column="email"/> <result property="createTime" column="create_time"/> </resultMap> <select id="selectByCondition" resultMap="userResultMap"> SELECT * FROM users <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="email != null"> AND email = #{email} </if> <if test="createTimeStart != null"> AND create_time >= #{createTimeStart} </if> <if test="createTimeEnd != null"> AND create_time <= #{createTimeEnd} </if> </where> ORDER BY id DESC </select> </mapper>3.2 动态SQL技巧
MyBatis强大的动态SQL能力是其核心竞争力之一。以下是几种常用的动态SQL写法:
条件查询(where + if)
<select id="searchUsers" resultType="User"> SELECT * FROM users <where> <if test="name != null"> name = #{name} </if> <if test="age != null"> AND age = #{age} </if> </where> </select>选择更新(set + if)
<update id="updateUser" parameterType="User"> UPDATE users <set> <if test="name != null">name=#{name},</if> <if test="email != null">email=#{email},</if> </set> WHERE id=#{id} </update>批量插入(foreach)
<insert id="batchInsert" parameterType="java.util.List"> INSERT INTO users (name, email) VALUES <foreach collection="list" item="user" separator=","> (#{user.name}, #{user.email}) </foreach> </insert>多条件选择(choose/when/otherwise)
<select id="selectByStatus" resultType="User"> SELECT * FROM users <where> <choose> <when test="status == 'active'"> AND active = 1 </when> <when test="status == 'inactive'"> AND active = 0 </when> <otherwise> AND active IS NOT NULL </otherwise> </choose> </where> </select>避坑指南:在foreach中使用集合参数时,务必注意collection属性的取值:
- 单参数List类型:collection="list"
- 单参数数组类型:collection="array"
- 多参数使用@Param注解:collection="参数名"
3.3 结果映射高级技巧
MyBatis的结果映射(resultMap)功能非常强大,可以处理各种复杂的映射场景。
一对一关联查询
<resultMap id="orderWithUserMap" type="Order"> <id property="id" column="order_id"/> <result property="orderNo" column="order_no"/> <!-- 关联用户对象 --> <association property="user" javaType="User"> <id property="id" column="user_id"/> <result property="name" column="user_name"/> </association> </resultMap> <select id="selectOrderWithUser" resultMap="orderWithUserMap"> SELECT o.id as order_id, o.order_no, u.id as user_id, u.name as user_name FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE o.id = #{id} </select>一对多关联查询
<resultMap id="userWithOrdersMap" type="User"> <id property="id" column="id"/> <result property="name" column="name"/> <!-- 订单集合 --> <collection property="orders" ofType="Order"> <id property="id" column="order_id"/> <result property="orderNo" column="order_no"/> </collection> </resultMap> <select id="selectUserWithOrders" resultMap="userWithOrdersMap"> SELECT u.id, u.name, o.id as order_id, o.order_no FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.id = #{id} </select>嵌套结果映射
<resultMap id="detailedOrderMap" type="Order"> <id property="id" column="id"/> <association property="user" resultMap="userMap"/> <collection property="items" resultMap="itemMap"/> </resultMap> <resultMap id="userMap" type="User"> <id property="id" column="user_id"/> <result property="name" column="user_name"/> </resultMap> <resultMap id="itemMap" type="OrderItem"> <id property="id" column="item_id"/> <result property="productName" column="product_name"/> </resultMap>枚举类型处理
public enum UserStatus { ACTIVE(1), INACTIVE(0), LOCKED(-1); private final int code; UserStatus(int code) { this.code = code; } public int getCode() { return code; } public static UserStatus fromCode(int code) { for (UserStatus status : values()) { if (status.code == code) { return status; } } throw new IllegalArgumentException("未知状态码: " + code); } }<resultMap id="userResultMap" type="User"> <result property="status" column="status" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/> </resultMap>4. 高级特性与性能优化
4.1 插件开发实战
MyBatis的插件机制允许我们在SQL执行的各个阶段插入自定义逻辑。最常见的插件应用场景包括:
- SQL性能监控
- 分页处理
- 敏感数据加解密
- 审计字段自动填充
下面是一个完整的SQL执行时间统计插件实现:
@Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class}) }) public class SqlCostTimeInterceptor implements Interceptor { private static final Logger logger = LoggerFactory.getLogger(SqlCostTimeInterceptor.class); @Override public Object intercept(Invocation invocation) throws Throwable { long startTime = System.currentTimeMillis(); try { return invocation.proceed(); } finally { long costTime = System.currentTimeMillis() - startTime; StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); String sql = statementHandler.getBoundSql().getSql(); if (costTime > 200) { logger.warn("SQL执行耗时: {}ms - {}", costTime, sql); } else { logger.debug("SQL执行耗时: {}ms - {}", costTime, sql); } } } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 可以读取配置参数 } }注册插件到MyBatis配置:
@Bean public SqlCostTimeInterceptor sqlCostTimeInterceptor() { return new SqlCostTimeInterceptor(); } @Bean public ConfigurationCustomizer configurationCustomizer() { return configuration -> { configuration.addInterceptor(sqlCostTimeInterceptor()); }; }4.2 二级缓存配置
MyBatis的二级缓存可以显著提升查询性能,但使用不当会导致脏读问题。以下是安全使用二级缓存的配置方式:
<!-- 在mybatis-config.xml中 --> <settings> <setting name="cacheEnabled" value="true"/> </settings> <!-- 在Mapper.xml中 --> <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>实际项目中,我建议:
- 只在读多写少的场景使用二级缓存
- 设置合理的flushInterval(如5分钟)
- 避免在分布式环境中使用本地缓存
- 对于关键业务数据,考虑手动控制缓存清除
4.3 批量操作优化
批量操作是性能优化的重点。以下是几种批量处理的正确姿势:
方式一:foreach批量插入
public interface UserMapper { void batchInsert(@Param("list") List<User> users); }<insert id="batchInsert"> INSERT INTO users (name, email) VALUES <foreach collection="list" item="user" separator=","> (#{user.name}, #{user.email}) </foreach> </insert>方式二:BATCH执行器
// 获取批量模式的SqlSession SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); try { UserMapper mapper = sqlSession.getMapper(UserMapper.class); for (User user : userList) { mapper.insert(user); } sqlSession.commit(); } finally { sqlSession.close(); }方式三:rewriteBatchedStatements在JDBC连接字符串中添加rewriteBatchedStatements=true可以显著提升MySQL批量插入性能:
jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true性能测试数据:在插入10000条记录时,普通插入耗时约12秒,批量模式仅需1.5秒,而rewriteBatchedStatements+批量模式仅需0.8秒。
4.4 分页查询实现
分页是Web应用中最常见的需求之一。以下是几种分页方案对比:
1. 使用PageHelper插件
// 引入依赖 <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.1</version> </dependency> // 使用示例 PageHelper.startPage(1, 10); // 第1页,每页10条 List<User> users = userMapper.selectAll(); PageInfo<User> pageInfo = new PageInfo<>(users);2. 手动实现分页
<select id="selectByPage" resultType="User"> SELECT * FROM users ORDER BY id LIMIT #{offset}, #{pageSize} </select>3. 使用MyBatis-Plus分页
// 配置分页插件 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } // 使用示例 Page<User> page = new Page<>(1, 10); userMapper.selectPage(page, null); List<User> records = page.getRecords();5. 生产环境最佳实践
5.1 SQL注入防护
虽然MyBatis使用预编译语句可以有效防止SQL注入,但不当使用仍然存在风险:
危险写法($符号直接拼接)
<select id="selectByOrder" resultType="User"> SELECT * FROM users ORDER BY ${columnName} </select>安全写法(使用#{}或白名单校验)
// 在Java代码中校验排序字段 private static final Set<String> ALLOWED_SORT_COLUMNS = Set.of("id", "name", "email", "create_time"); public List<User> selectUsers(String sortBy) { if (!ALLOWED_SORT_COLUMNS.contains(sortBy)) { sortBy = "id"; } return userMapper.selectByOrder(sortBy); }<select id="selectByOrder" resultType="User"> SELECT * FROM users ORDER BY ${columnName} <!-- 注意:即使使用白名单校验,$仍然有风险,最好在应用层排序 --> </select>5.2 事务管理
Spring与MyBatis整合后,可以使用Spring的声明式事务管理:
@Service public class UserService { @Autowired private UserMapper userMapper; @Transactional(rollbackFor = Exception.class) public void updateUser(User user) { // 业务逻辑... userMapper.update(user); // 更多数据库操作... } @Transactional(propagation = Propagation.REQUIRES_NEW) public void logOperation(OperationLog log) { // 独立事务记录日志 } }常见事务问题排查:
- 确保使用的是Spring代理对象(调用事务方法的类不能被final修饰)
- 检查异常是否被捕获未抛出(默认只回滚RuntimeException)
- 确认数据源是否配置了事务管理器
- 检查@Transactional是否添加在public方法上
5.3 监控与调优
生产环境必备的监控措施:
1. Druid监控
spring: datasource: druid: filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 1000 web-stat-filter: enabled: true aop-patterns: com.example.mapper.*2. 慢SQL日志
<!-- mybatis-config.xml --> <settings> <setting name="logImpl" value="SLF4J"/> </settings> <!-- logback.xml --> <logger name="org.mybatis" level="DEBUG"/> <logger name="java.sql" level="DEBUG"/>3. 连接池监控指标
@RestController public class DruidStatController { @Autowired private DataSource dataSource; @GetMapping("/druid/stat") public Object druidStat() { DruidDataSource druidDataSource = (DruidDataSource) dataSource; Map<String, Object> stat = new HashMap<>(); stat.put("activeCount", druidDataSource.getActiveCount()); stat.put("activePeak", druidDataSource.getActivePeak()); stat.put("poolingCount", druidDataSource.getPoolingCount()); stat.put("connectCount", druidDataSource.getConnectCount()); stat.put("waitThreadCount", druidDataSource.getWaitThreadCount()); return stat; } }5.4 常见问题解决方案
问题1:Mapper接口无法注入
- 检查@MapperScan是否配置正确
- 确认Mapper接口是否有@Repository或@Mapper注解
- 检查XML文件是否在classpath对应路径
问题2:字段值为null
- 检查数据库字段名与Java属性名是否匹配(开启mapUnderscoreToCamelCase)
- 确认resultMap配置是否正确
- 检查Getter/Setter方法是否存在
问题3:事务不生效
- 确认是否抛出了RuntimeException
- 检查是否调用了同类中的其他方法(自调用问题)
- 确认数据源是否配置了事务管理器
问题4:分页插件冲突
- 检查是否引入了多个分页插件(如PageHelper和MyBatis-Plus分页)
- 确认插件执行顺序是否正确
问题5:XML特殊字符处理
<!-- 错误写法 --> <select id="selectActiveUsers"> SELECT * FROM users WHERE status = 1 AND age < 30 </select> <!-- 正确写法 --> <select id="selectActiveUsers"> SELECT * FROM users WHERE status = 1 AND age < 30 </select> <!-- 或者使用CDATA --> <select id="selectActiveUsers"> <![CDATA[ SELECT * FROM users WHERE status = 1 AND age < 30 ]]> </select>6. 扩展与集成方案
6.1 与MyBatis-Plus整合
MyBatis-Plus是MyBatis的增强工具,提供了大量开箱即用的功能:
// 基础Mapper接口 public interface UserMapper extends BaseMapper<User> { // 已经内置了基本的CRUD方法 } // 服务层示例 @Service public class UserService { @Autowired private UserMapper userMapper; public List<User> selectByCondition(UserQuery query) { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(query.getName()), "name", query.getName()) .eq(query.getStatus() != null, "status", query.getStatus()) .between(query.getStartTime() != null && query.getEndTime() != null, "create_time", query.getStartTime(), query.getEndTime()); return userMapper.selectList(wrapper); } }6.2 多数据源配置
大型项目往往需要访问多个数据源:
@Configuration @MapperScan(basePackages = "com.example.primary.mapper", sqlSessionFactoryRef = "primarySqlSessionFactory") public class PrimaryDataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.primary") public DataSource primaryDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean public SqlSessionFactory primarySqlSessionFactory( @Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); return factoryBean.getObject(); } @Bean public DataSourceTransactionManager primaryTransactionManager( @Qualifier("primaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } } // 第二个数据源配置类似,使用不同的包扫描路径和Bean名称6.3 代码生成器配置
MyBatis Generator可以自动生成基础代码:
<!-- generatorConfig.xml --> <context id="mysql" targetRuntime="MyBatis3"> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/mybatis_demo" userId="root" password="123456"/> <javaModelGenerator targetPackage="com.example.entity" targetProject="src/main/java"/> <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/> <javaClientGenerator type="XMLMAPPER" targetPackage="com.example.mapper" targetProject="src/main/java"/> <table tableName="user" domainObjectName="User"/> <table tableName="order" domainObjectName="Order"/> </context>运行生成器:
public class MyBatisGenerator { public static void main(String[] args) throws Exception { List<String> warnings = new ArrayList<>(); ConfigurationParser cp = new ConfigurationParser(warnings); Configuration config = cp.parseConfiguration( MyBatisGenerator.class.getResourceAsStream("/generatorConfig.xml")); DefaultShellCallback callback = new DefaultShellCallback(true); MyBatisGenerator generator = new MyBatisGenerator(config, callback, warnings); generator.generate(null); warnings.forEach(System.out::println); } }6.4 与Spring Boot深度整合
Spring Boot提供了自动配置的MyBatis starter:
# application.yml mybatis: mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true default-fetch-size: 100 default-statement-timeout: 30自定义配置类:
@Configuration public class MyBatisCustomConfig { @Bean public ConfigurationCustomizer mybatisConfigurationCustomizer() { return configuration -> { // 添加自定义类型处理器 configuration.getTypeHandlerRegistry() .register(MyEnumTypeHandler.class); // 添加插件 configuration.addInterceptor(new SqlCostTimeInterceptor()); }; } }7. 实战案例:用户管理系统
7.1 领域模型设计
// 用户实体 @Data public class User { private Long id; private String username; private String password; private String email; private UserStatus status; private Date createTime; private Date updateTime; } // 查询条件封装 @Data public class UserQuery { private String username; private String email; private UserStatus status; private Date createTimeStart; private Date createTimeEnd; private Integer pageNum = 1; private Integer pageSize = 10; }7.2 核心Mapper实现
public interface UserMapper { // 基础CRUD @Select("SELECT * FROM user WHERE id = #{id}") User selectById(Long id); @Options(useGeneratedKeys = true, keyProperty = "id") @Insert("INSERT INTO user(username,password,email,status) " + "VALUES(#{username},#{password},#{email},#{status})") int insert(User user); @Update("UPDATE user SET username=#{username},email=#{email}," + "status=#{status} WHERE id=#{id}") int update(User user); @Delete("DELETE FROM user WHERE id=#{id}") int delete(Long id); // 条件查询 List<User> selectByCondition(UserQuery query); // 批量操作 int batchInsert(@Param("list") List<User> users); // 关联查询 @Select("SELECT u.* FROM user u JOIN user_role ur ON u.id = ur.user_id " + "WHERE ur.role_id = #{roleId}") List<User> selectByRoleId(Long roleId); }对应的XML映射文件:
<select id="selectByCondition" resultType="User"> SELECT * FROM user <where> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <if test="email != null and email != ''"> AND email = #{email} </if> <if test="status != null"> AND status = #{status} </if> <if test="createTimeStart != null"> AND create_time >= #{createTimeStart} </if> <if test="createTimeEnd != null"> AND create_time <= #{createTimeEnd} </if> </where> ORDER BY id DESC LIMIT #{offset}, #{pageSize} </select>7.3 服务层实现
@Service @Transactional(readOnly = true) public class UserService { @Autowired private UserMapper userMapper; public PageInfo<User> queryUsers(UserQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<User> users = userMapper.selectByCondition(query); return new PageInfo<>(users); } @Transactional(rollbackFor = Exception.class) public void createUser(User user) { user.setCreateTime(new Date()); user.setUpdateTime(new Date()); userMapper.insert(user); } @Transactional(rollbackFor = Exception.class) public void updateUser(User user) { user.setUpdateTime(new Date()); userMapper.update(user); } @Transactional(rollbackFor = Exception.class) public void deleteUser(Long id) { userMapper.delete(id); } @Transactional(rollbackFor = Exception.class) public void batchCreateUsers(List<User> users) { users.forEach(user -> { user.setCreateTime(new Date()); user.setUpdateTime(new Date()); }); userMapper.batchInsert(users); } }7.4 控制器层
@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping public Result<PageInfo<User>> listUsers(UserQuery query) { return Result.success(userService.queryUsers(query)); } @PostMapping public Result<Void> createUser(@RequestBody @Valid User user) { userService.createUser(user); return Result.success(); } @PutMapping("/{id}") public Result<Void> updateUser(@PathVariable Long id, @RequestBody @Valid User user) { user.setId(id); userService.updateUser(user); return Result.success(); } @DeleteMapping("/{id}") public Result<Void> deleteUser(@PathVariable Long id) { userService.deleteUser(id); return Result.success(); } }8. 性能调优实战
8.1 SQL执行计划分析
通过EXPLAIN分析SQL性能:
public interface ExplainMapper { @Select("EXPLAIN ${sql}") List<Map<String, Object>> explain(@Param("sql") String sql); } // 使用示例 public void analyzeQuery() { String sql = "SELECT * FROM user WHERE status = 1 ORDER BY create_time DESC"; List<Map<String, Object>> result = explainMapper.explain(sql); result.forEach(row -> { System.out.println("id: " + row.get("id")); System.out.println("select_type: " + row.get("select_type")); System.out.println("table: " + row.get("table")); System.out.println("type: " + row.get("type")); System.out.println("possible_keys: " + row.get("possible_keys")); System.out.println("key: " + row.get("key")); System.out.println("rows: " + row.get("rows")); System.out.println("Extra: " + row.get("Extra")); }); }关键指标解读:
- type:ALL表示全表扫描,应优化为range或ref
- key:实际使用的索引
- rows:预估扫描行数
- Extra:Using filesort表示需要优化排序
8.2 索引优化策略
1. 复合索引设计
-- 为常用查询条件创建复合索引 ALTER TABLE user ADD INDEX idx_status_create_time (status, create_time); -- 覆盖索引优化 ALTER TABLE user ADD INDEX idx_username_email (username, email);2. 索引使用要点
- 遵循最左前缀原则
- 避免在索引列上使用函数
- 字符串字段考虑前缀索引
- 区分度低的字段不适合建索引
8.3 连接池优化
Druid连接池关键参数调优:
spring: datasource: druid: # 初始连接数 initial-size: 10 # 最小空闲连接 min-idle: 10 # 最大活跃连接 max-active: 50 # 获取连接等待超时时间(毫秒) max-wait: 60000 # 配置间隔多久检测空闲连接(毫秒) time-between-eviction-runs-millis: 60000 # 连接最小生存时间(毫秒) min-evictable-idle-time-millis: 300000 # 测试连接有效性的SQL validation-query: SELECT 1 # 申请连接时执行validationQuery检测连接有效性 test-on-borrow: false # 归还连接时执行validationQuery检测连接有效性 test-on-return: false # 空闲时检测连接有效性 test-while-idle: true8.4 缓存策略优化
**多级