MyBatisPlus的核心特性
在传统 MyBatis 中,即便是简单的增删改查(CRUD),你也需要编写 XML 映射文件或大量的 SQL 语句。而在 MyBatis-Plus 中:
- 无侵入/损耗小:它只是 MyBatis 的插件,不会影响你原有的业务逻辑。
- 通用 CRUD:内置了通用的 Mapper 和 Service。只需继承
BaseMapper<T>,即可直接调用insert、update、selectById等方法,无需编写任何 SQL。 - 条件构造器 (Wrapper) :通过链式调用(如
.eq("name", "Jack").gt("age", 20))自动生成动态 SQL,极大地减少了代码量。 - 自动分页:内置分页插件,只需简单配置,即可实现物理分页(支持 MySQL, Oracle, PostgreSQL 等主流数据库)。
工作原理
MyBatis能够实现继承BaseMapper<T>就可以完成自动化的核心秘密在于:启动时的SQL注入机制
当你启动一个 Spring Boot 项目时,MyBatis-Plus 会进行一系列“幕后工作”:
-
类扫描:MP 会扫描所有继承了
BaseMapper<T>的接口。 -
泛型解析:它会通过反射获取
BaseMapper<V>中的泛型V,从而拿到你的数据库实体类(Entity)。 -
内建方法映射:MP 内部定义了一套
AbstractMethod列表(如Insert,DeleteById,SelectUpdate等)。 -
SQL 生成与注入:
- 它会根据实体类上的注解(如
@TableId,@TableField)解析出表名和字段名。 - 将这些信息填充进预设的 SQL 模板中。
- 最后,它把生成好的 SQL 语句手动注册到 MyBatis 核心的
MappedStatement缓存中。
- 它会根据实体类上的注解(如
MyBatis-Plus 的核心 SQL 注入是在 Spring 容器启动过程中完成的,而不是在调用时。所以就算有几千张表,也只是会启动较慢。 当你调用
userMapper.selectById(1)时,其实是 MyBatis 的 JDK 动态代理 在运行。它会去缓存里找 MP 帮你注入好的那段 SQL,然后执行。
JDK动态代理
动态代理就是在程序运行期间,根据需要动态地创建一个“代理对象”,代替“真实对象”去执行任务,并在此过程中偷偷增加一些额外的功能。
为什么需要“动态”代理
在大型软件开发中,我们经常需要给成百上千个方法添加统一的功能,比如:
-
日志记录(每个接口调用前打印参数)。
-
事务控制(MyBatis 开启和提交事务)。
-
权限校验(判断当前用户是否有权访问)。
如果没有动态代理: 你必须在每一个方法里手动写 log.info(...)。如果有 1000 个方法,你就得写 1000 次。
有了动态代理: 你只需要写一个“代理规则”,程序运行的时候,它会自动给这 1000 个方法套上一个“壳”。
Java动态代理的实现方式
A. JDK动态代理(MyBatis核心)
-
要求:真实对象必须实现了一个接口(比如你的
UserMapper接口)。 -
原理:利用反射机制在内存中创建一个实现了相同接口的新类。
-
MyBatis-Plus 的应用:你定义的
UserMapper只是个接口,并没有实现类。当你调用selectById时,其实是 JDK 动态代理生成了一个代理类,拦截了你的调用,并去执行了 MP 注入的 SQL。
B. CGLIB 代理
-
要求:不需要接口,通过继承真实对象来生成代理类。
-
原理:底层通过修改字节码(ASM)生成子类。
-
应用:Spring 在给没有接口的普通的
@Service类加事务(@Transactional)时,通常使用 CGLIB。
代码实例
假设你有一个接口和一个实现类,你想在不改动代码的情况下统计执行时间
// 1. 拦截器定义规则
InvocationHandler handler = (proxy, method, args) -> {
System.out.println("方法执行前:记录开始时间"); // 增强逻辑
Object result = method.invoke(target, args); // 调用真实技能
System.out.println("方法执行后:记录结束时间"); // 增强逻辑
return result;
};
// 2. 动态生成代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{UserService.class},
handler
);
// 3. 调用时,其实走的是代理
proxy.saveUser();
ServiceImpl
在 MyBatis-Plus 的设计中,ServiceImpl 不是一个接口,而是一个具体的实现类。它极大地简化了业务层的代码开发。
class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article>:
M (Mapper): 告诉 Service 你的底层数据库操作用哪个 Mapper(即ArticleMapper)。T (Entity): 告诉 Service 你的操作对象是哪个实体(即Article)。
为什么要有ServiceImpl?
| 维度 | Mapper 层 (BaseMapper) | Service 层 (IService/ServiceImpl) |
|---|---|---|
| 粒度 | 原子操作:对应单条 SQL(增删改查)。 | 业务操作:可能包含多步逻辑、校验、事务。 |
| 功能数量 | 方法较少(约 17 个)。 | 功能极多(包含批量操作、链式调用)。 |
| 典型方法 | insert, selectById | saveOrUpdateBatch (批量存或更新), page (分页)。 |
| 现实意义 | 负责**“怎么存”**。 | 负责**“存什么、什么时候存、存完干什么”**。 |
分页
- MyBatisPlus开启分页配置:
@Configuration
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
当调用service.page时,这个拦截器会拦截住原本的sql,然后根据传入的分页参数,把sql改写成:
-
SELECT COUNT(*) FROM article(先查总数)。 -
SELECT * FROM article LIMIT 0, 10(再查具体数据)。
最后把改写后的物理分页 SQL 发给数据库。
- 请求参数
@Data
public class PageRequestVo {
private long current = 1;
private long size = 10;
private String sortField;
private boolean isAsc = false;
private String keyword;
}
- 实体类
public IPage<Article> list(PageRequestVo pageRequestVo) {
Page<Article> page = new Page<>(pageRequestVo.getCurrent(), pageRequestVo.getSize());
if (StringUtils.hasText(pageRequestVo.getSortField())){
OrderItem orderItem = pageRequestVo.isAsc() ?
OrderItem.asc(pageRequestVo.getSortField()) : OrderItem.desc(pageRequestVo.getSortField());
page.addOrder(orderItem);
}
LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.hasText(pageRequestVo.getKeyword()), Article::getArticleTitle, pageRequestVo.getKeyword());
return this.page(page, wrapper);
}