一、说明
咱们使用SpringBoot的时候,经常使用xxx-starter,比如:mybatis-spring-boot-starter、spring-boot-starter等等,然后就可以再加载maven后,使用相应的对象。你有没有这样的疑问:为什么一个简单地引入一个starter就可以使用某个对象?今天我们来揭开这个面纱,并从零开始写一个自定义的starter,并且运用起来。介绍文章见另外一篇。
下面我就以我仿mybatis-plus的自定义starter的demo为例。
二、代码结构
mybatis-generic-starter/ # Starter 项目根目录
├── pom.xml # Maven 依赖配置(核心:指定 Starter 规范、自动装配依赖)
└── src/
└── main/
├── java/
│ └── com/
│ └── customer/
│ └── mp/
│ ├── base/ # 你的通用代码(注解、Mapper、工具类、拦截器)
│ │ ├── annotation/ # TableName、TableField 注解
│ │ ├── interceptor/ # GenericSqlStatementHandlerInterceptor
│ │ ├── mapper/ # BaseMapper 接口
│ │ └── util/ # ReflectionUtil 工具类
│ └── autoconfigure/ # 自动装配核心包(Starter 关键)
│ ├── MyBatisGenericAutoConfiguration.java # 自动配置类
│ └── MyBatisGenericProperties.java # 可选:配置属性类
└── resources/
└── META-INF/
└── spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports # 自动装配入口三、POM依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.customer.mp</groupId>
<artifactId>customer-mp</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>customer-mp</name>
<url>http://maven.apache.org</url>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.12.RELEASE</spring-boot.version>
</properties>
<dependencies>
<!-- Spring Boot 自动装配核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- Spring Boot 配置注解处理器(可选,用于配置属性提示) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- MyBatis Spring Boot 启动器(自动配置) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version> <!-- 与Spring Boot 2.3.12.RELEASE兼容 -->
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<scope>runtime</scope>
</dependency>
<!-- 日志依赖(兼容 Spring Boot 默认日志) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<skip>true</skip> <!-- Starter 不是可运行项目,跳过主类检测 -->
</configuration>
</plugin>
<!-- Maven 编译插件(指定 JDK 1.8) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
四、注解
TableName
package com.customer.mp.base.anotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 指定实体对应的数据库表名
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableName {
String value(); // 表名
}TableField
package com.customer.mp.base.anotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 指定普通字段映射
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableField {
String value() default ""; // 列名
boolean exist() default true; // 是否为数据库字段
}TableId
package com.customer.mp.base.anotation;
import com.customer.mp.base.enums.IdType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 指定主键字段
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableId {
String value() default ""; // 主键列名(默认驼峰转下划线)
IdType type() default IdType.ASSIGN_ID; // 主键生成策略
}五、分页对象
package com.customer.mp.base.bean;
import java.util.List;
public class Page<T> {
private int pageNum; // 页码(默认第1页)
private int pageSize; // 每页条数(默认10条)
private long total; // 总记录数
private List<T> records; // 分页结果列表
public Page(int pageNum, int pageSize, long total, List<T> records) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.total = total;
this.records = records;
}
public Page() {
}
// 构造方法 + getter/setter 省略
public Page(int pageNum, int pageSize) {
this.pageNum = pageNum < 1 ? 1 : pageNum;
this.pageSize = pageSize < 1 ? 10 : pageSize;
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
public List<T> getRecords() {
return records;
}
public void setRecords(List<T> records) {
this.records = records;
}
}六、ID生成策略
ID生成策略枚举:
package com.customer.mp.base.enums;
// 主键生成策略枚举
public enum IdType {
AUTO, // 数据库自增
ASSIGN_ID, // 自定义雪花算法
ASSIGN_UUID // UUID
}ID生成策略接口及实现:
package com.customer.mp.base.service;
public interface IdGenerator {
Object generateId();
}package com.customer.mp.base.service.impl;
import com.customer.mp.base.service.IdGenerator;
// UUID生成器
public class UUIDIdGenerator implements IdGenerator {
@Override
public Object generateId() {
return java.util.UUID.randomUUID().toString().replace("-", "");
}
}package com.customer.mp.base.service.impl;
import com.customer.mp.base.service.IdGenerator;
// 雪花算法生成器(简化实现,实际需考虑分布式唯一)
public class SnowflakeIdGenerator implements IdGenerator {
@Override
public Object generateId() {
// 简化示例:返回当前时间戳 + 随机数(实际需实现标准雪花算法)
return System.currentTimeMillis() + (long) (Math.random() * 1000);
}
}七、反射工具类
package com.customer.mp.base.utils;
import com.customer.mp.base.anotation.TableId;
import com.customer.mp.base.anotation.TableName;
import com.customer.mp.base.mapper.BaseMapper;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
/**
* 通用反射工具类:解析泛型、表名、列名
*/
public class ReflectionUtil {
/**
* 通用方法:从Mapper接口中解析泛型实体类类型
* @param mapperClass Mapper接口(继承BaseMapper<T>)
* @return 泛型实体类Class
*/
public static Class<?> getMapperGenericType(Class<?> mapperClass) {
// 获取Mapper接口的父接口(BaseMapper<T>)
Type[] genericInterfaces = mapperClass.getGenericInterfaces();
for (Type genericInterface : genericInterfaces) {
if (genericInterface instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
// 判断是否是BaseMapper
if (parameterizedType.getRawType() == BaseMapper.class) {
// 获取泛型参数(T)
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
if (actualTypeArguments != null && actualTypeArguments.length > 0) {
return (Class<?>) actualTypeArguments[0];
}
}
}
}
throw new RuntimeException("无法从Mapper接口中解析泛型实体类");
}
/**
* 通用方法:从实体类中解析数据库表名
* @param entityClass 实体类Class
* @return 数据库表名
*/
public static String getTableName(Class<?> entityClass) {
TableName tableNameAnnotation = entityClass.getAnnotation(TableName.class);
if (tableNameAnnotation != null) {
return tableNameAnnotation.value();
}
// 默认:实体类名转下划线(如UserInfo → user_info,可选扩展)
return camelToUnderline(entityClass.getSimpleName());
}
/**
* 通用方法:从实体类中解析数据库列名(所有属性对应列名)
* @param entityClass 实体类Class
* @return 列名字符串(如 "id, username, password, age")
*/
public static String getColumnNames(Class<?> entityClass) {
Field[] fields = entityClass.getDeclaredFields();
List<String> columnNames = new ArrayList<>();
for (Field field : fields) {
// 列名默认与属性名一致,可选扩展为驼峰转下划线
String columnName = camelToUnderline(field.getName());
columnNames.add(columnName);
}
return String.join(", ", columnNames);
}
/**
* 通用方法:从实体类中解析主键列名
* @param entityClass 实体类Class
* @return 主键列名
*/
public static String getIdColumnName(Class<?> entityClass) {
Field[] fields = entityClass.getDeclaredFields();
for (Field field : fields) {
TableId idAnnotation = field.getAnnotation(TableId.class);
if (idAnnotation != null) {
// 若注解指定了列名,使用指定值;否则使用属性名转下划线
String columnName = idAnnotation.value().trim();
return columnName.isEmpty() ? camelToUnderline(field.getName()) : columnName;
}
}
// 默认主键列名:id
return "id";
}
/**
* 辅助方法:驼峰命名转下划线命名(通用)
* @param camelName 驼峰名(如userName)
* @return 下划线名(如user_name)
*/
private static String camelToUnderline(String camelName) {
if (camelName == null || camelName.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < camelName.length(); i++) {
char c = camelName.charAt(i);
if (Character.isUpperCase(c)) {
sb.append("_").append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
}八、表及字段处理
package com.customer.mp.base.table;
import com.customer.mp.base.enums.IdType;
import java.util.Map;
public class TableInfo {
private Class<?> entityClass; // 实体类Class
private String tableName; // 数据库表名
private String idColumn; // 主键列名
private String idFieldName; // 实体主键字段名
private IdType idType; // 主键生成策略
private Map<String, String> fieldColumnMap; // 普通字段-列名映射(字段名→列名)
private Map<String, Boolean> fieldExistMap; // 字段是否为数据库字段
public TableInfo(Class<?> entityClass, String tableName, String idColumn, String idFieldName, IdType idType, Map<String, String> fieldColumnMap, Map<String, Boolean> fieldExistMap) {
this.entityClass = entityClass;
this.tableName = tableName;
this.idColumn = idColumn;
this.idFieldName = idFieldName;
this.idType = idType;
this.fieldColumnMap = fieldColumnMap;
this.fieldExistMap = fieldExistMap;
}
public TableInfo() {
}
public Class<?> getEntityClass() {
return entityClass;
}
public void setEntityClass(Class<?> entityClass) {
this.entityClass = entityClass;
}
public String getTableName() {
return tableName;
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
public String getIdColumn() {
return idColumn;
}
public void setIdColumn(String idColumn) {
this.idColumn = idColumn;
}
public String getIdFieldName() {
return idFieldName;
}
public void setIdFieldName(String idFieldName) {
this.idFieldName = idFieldName;
}
public IdType getIdType() {
return idType;
}
public void setIdType(IdType idType) {
this.idType = idType;
}
public Map<String, String> getFieldColumnMap() {
return fieldColumnMap;
}
public void setFieldColumnMap(Map<String, String> fieldColumnMap) {
this.fieldColumnMap = fieldColumnMap;
}
public Map<String, Boolean> getFieldExistMap() {
return fieldExistMap;
}
public void setFieldExistMap(Map<String, Boolean> fieldExistMap) {
this.fieldExistMap = fieldExistMap;
}
}package com.customer.mp.base.table;
import com.customer.mp.base.anotation.TableField;
import com.customer.mp.base.anotation.TableId;
import com.customer.mp.base.anotation.TableName;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class TableInfoParser {
// 解析实体类,返回TableInfo
public static <T> TableInfo parse(Class<T> entityClass) {
TableInfo tableInfo = new TableInfo();
tableInfo.setEntityClass(entityClass);
// 解析@TableName注解
TableName tableNameAnnotation = entityClass.getAnnotation(TableName.class);
if (tableNameAnnotation != null) {
tableInfo.setTableName(tableNameAnnotation.value());
} else {
// 无注解时,驼峰转下划线作为表名(约定大于配置)
tableInfo.setTableName(camelToUnderline(entityClass.getSimpleName()));
}
// 解析字段(主键 + 普通字段)
Field[] fields = entityClass.getDeclaredFields();
Map<String, String> fieldColumnMap = new HashMap<>();
Map<String, Boolean> fieldExistMap = new HashMap<>();
for (Field field : fields) {
String fieldName = field.getName();
// 解析@TableId
TableId tableIdAnnotation = field.getAnnotation(TableId.class);
if (tableIdAnnotation != null) {
tableInfo.setIdFieldName(fieldName);
// 主键列名:注解指定 > 驼峰转下划线
String idColumn = tableIdAnnotation.value().isEmpty()
? camelToUnderline(fieldName)
: tableIdAnnotation.value();
tableInfo.setIdColumn(idColumn);
tableInfo.setIdType(tableIdAnnotation.type());
}
// 解析@TableField
TableField tableFieldAnnotation = field.getAnnotation(TableField.class);
if (tableFieldAnnotation != null) {
fieldExistMap.put(fieldName, tableFieldAnnotation.exist());
if (!tableFieldAnnotation.value().isEmpty()) {
fieldColumnMap.put(fieldName, tableFieldAnnotation.value());
continue;
}
}
// 普通字段:驼峰转下划线
fieldColumnMap.put(fieldName, camelToUnderline(fieldName));
fieldExistMap.put(fieldName, true);
}
tableInfo.setFieldColumnMap(fieldColumnMap);
tableInfo.setFieldExistMap(fieldExistMap);
return tableInfo;
}
// 驼峰转下划线工具方法
private static String camelToUnderline(String str) {
if (str == null || str.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
sb.append(Character.toLowerCase(str.charAt(0)));
for (int i = 1; i < str.length(); i++) {
char c = str.charAt(i);
if (Character.isUpperCase(c)) {
sb.append("_").append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
}九、wrapper
package com.customer.mp.base.wrapper;
import com.customer.mp.base.table.TableInfo;
import com.customer.mp.base.table.TableInfoParser;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractWrapper<T> {
// 存储条件片段(如 "name = ?")
protected List<String> conditionSegments = new ArrayList<>();
// 存储条件参数(对应?占位符,避免SQL注入)
protected List<Object> params = new ArrayList<>();
// 实体映射元数据
protected TableInfo tableInfo;
public AbstractWrapper(Class<T> entityClass) {
this.tableInfo = TableInfoParser.parse(entityClass);
}
// 等值查询:eq("userName", "张三") → "user_name = ?"
public AbstractWrapper<T> eq(String fieldName, Object value) {
if (value == null) {
return this;
}
// 获取字段对应的列名
String columnName = tableInfo.getFieldColumnMap().get(fieldName);
// 添加条件片段和参数
conditionSegments.add(columnName + " = ?");
params.add(value);
return this;
}
// 模糊查询:like("userName", "张") → "user_name LIKE ?"
public AbstractWrapper<T> like(String fieldName, Object value) {
if (value == null) {
return this;
}
String columnName = tableInfo.getFieldColumnMap().get(fieldName);
conditionSegments.add(columnName + " LIKE ?");
params.add("%" + value + "%");
return this;
}
// 其他条件方法(ne、gt、and、or等)类似,按需扩展...
// 构建WHERE条件片段
public String buildWhereSql() {
if (conditionSegments.isEmpty()) {
return "";
}
return "WHERE " + String.join(" AND ", conditionSegments);
}
// 获取参数列表
public List<Object> getParams() {
return params;
}
}package com.customer.mp.base.wrapper;
public class QueryWrapper<T> extends AbstractWrapper<T> {
public QueryWrapper(Class<T> entityClass) {
super(entityClass);
}
// 静态创建方法,简化使用
public static <T> QueryWrapper<T> create(Class<T> entityClass) {
return new QueryWrapper<>(entityClass);
}
}十、Mapper
package com.customer.mp.base.mapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 通用BaseMapper:添加空注解SQL,让MyBatis完成方法绑定
*/
public interface BaseMapper<T> {
// 空INSERT注解:仅用于MyBatis方法绑定,后续拦截器会替换为合法SQL
@Insert("")
int insert(T entity);
// 空DELETE注解:仅用于MyBatis方法绑定
@Delete("")
int deleteById(Object id);
// 空SELECT注解:仅用于MyBatis方法绑定
@Select("")
T selectById(Object id);
// 空SELECT注解:仅用于MyBatis方法绑定(解决selectList方法未找到问题)
@Select("")
List<T> selectList();
}十一、拦截器
package com.customer.mp.base.interceptor;
import com.customer.mp.base.mapper.BaseMapper;
import com.customer.mp.base.utils.ReflectionUtil;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.util.Properties;
/**
* 拦截StatementHandler:直接在创建PreparedStatement时使用修改后的合法SQL
* 彻底解决MappedStatement原始SQL为空的问题
*/
@Intercepts({
@Signature(type = org.apache.ibatis.executor.statement.StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})
})
public class GenericSqlStatementHandlerInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(GenericSqlStatementHandlerInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取StatementHandler(实际是RoutingStatementHandler)
org.apache.ibatis.executor.statement.StatementHandler statementHandler =
(org.apache.ibatis.executor.statement.StatementHandler) invocation.getTarget();
// 2. 获取原始BoundSql
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql().trim();
// 3. 获取MappedStatement(通过反射从StatementHandler中提取)
MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
String mapperMethodId = mappedStatement.getId();
// 4. 仅处理继承BaseMapper的Mapper方法
String mapperClassName = mapperMethodId.substring(0, mapperMethodId.lastIndexOf("."));
Class<?> mapperClass = Class.forName(mapperClassName);
if (!BaseMapper.class.isAssignableFrom(mapperClass)) {
return invocation.proceed();
}
// 5. 通用解析:表名、列名、主键列名
Class<?> entityClass = ReflectionUtil.getMapperGenericType(mapperClass);
String tableName = ReflectionUtil.getTableName(entityClass);
String columnNames = ReflectionUtil.getColumnNames(entityClass);
String idColumnName = ReflectionUtil.getIdColumnName(entityClass);
String methodName = mapperMethodId.substring(mapperMethodId.lastIndexOf(".") + 1);
// 6. 构建合法SQL(与之前一致)
String newSql = buildGenericSql(mappedStatement.getSqlCommandType(), methodName, tableName, columnNames, idColumnName);
if (newSql == null || newSql.trim().isEmpty()) {
logger.error("SQL构建失败,使用兜底SQL");
newSql = "SELECT 1 FROM DUAL";
}
logger.info("最终执行SQL:{}", newSql);
// 7. 强制修改BoundSql的SQL(关键:此时修改的SQL会直接用于创建PreparedStatement)
MetaObject metaBoundSql = SystemMetaObject.forObject(boundSql);
metaBoundSql.setValue("sql", newSql);
// 8. 执行原始prepare方法,创建PreparedStatement(此时使用的是修改后的SQL)
return invocation.proceed();
}
/**
* 复用之前的SQL构建逻辑
*/
private String buildGenericSql(SqlCommandType sqlCommandType, String methodName,
String tableName, String columnNames, String idColumnName) {
if (tableName == null || tableName.trim().isEmpty() || columnNames == null || columnNames.trim().isEmpty()) {
return null;
}
switch (sqlCommandType) {
case SELECT:
if ("selectList".equalsIgnoreCase(methodName)) {
return String.format("SELECT %s FROM %s", columnNames, tableName);
} else if ("selectById".equalsIgnoreCase(methodName)) {
return String.format("SELECT %s FROM %s WHERE %s = #{id}", columnNames, tableName, idColumnName);
}
break;
case DELETE:
if ("deleteById".equalsIgnoreCase(methodName)) {
return String.format("DELETE FROM %s WHERE %s = #{id}", tableName, idColumnName);
}
break;
case INSERT:
if ("insert".equalsIgnoreCase(methodName)) {
String[] columns = columnNames.split(", ");
StringBuilder placeholders = new StringBuilder();
for (int i = 0; i < columns.length; i++) {
String propertyName = underlineToCamel(columns[i]);
placeholders.append(String.format("#{entity.%s}", propertyName));
if (i < columns.length - 1) {
placeholders.append(", ");
}
}
return String.format("INSERT INTO %s (%s) VALUES (%s)", tableName, columnNames, placeholders);
}
break;
default:
break;
}
return "SELECT 1 FROM DUAL";
}
/**
* 下划线转驼峰(复用)
*/
private String underlineToCamel(String underlineName) {
if (underlineName == null || underlineName.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
boolean needUpper = false;
for (int i = 0; i < underlineName.length(); i++) {
char c = underlineName.charAt(i);
if (c == '_') {
needUpper = true;
continue;
}
if (needUpper) {
sb.append(Character.toUpperCase(c));
needUpper = false;
} else {
sb.append(c);
}
}
return sb.toString();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}package com.customer.mp.base.interceptor;
import com.customer.mp.base.bean.Page;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.util.Properties;
// 拦截 StatementHandler 的 prepare 方法
@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)})
public class PaginationInterceptor implements Interceptor {
/**
* 拦截核心逻辑:修改 SQL,添加分页条件
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取目标对象 StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 2. 通过 MetaObject 反射获取 StatementHandler 的内部属性
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 3. 获取原始 SQL 语句
String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
// 4. 获取分页参数(此处简化,实际可从参数中提取 Page 对象)
Page<?> page = getPageParam(metaObject);
if (page != null && page.getPageNum() > 0 && page.getPageSize() > 0) {
// 5. 构建 MySQL 分页 SQL(拼接 LIMIT)
String paginationSql = buildMysqlPaginationSql(originalSql, page);
// 6. 替换原始 SQL
metaObject.setValue("delegate.boundSql.sql", paginationSql);
// 7. 统计总记录数(简化实现,实际需执行 count SQL)
long total = countTotalRecords(originalSql, invocation, metaObject);
page.setTotal(total);
}
// 执行原始方法
return invocation.proceed();
}
/**
* 提取 Page 分页参数
*/
private Page<?> getPageParam(MetaObject metaObject) {
Object parameterObject = metaObject.getValue("delegate.boundSql.parameterObject");
if (parameterObject instanceof Page<?>) {
return (Page<?>) parameterObject;
}
return null;
}
/**
* 构建 MySQL 分页 SQL
*/
private String buildMysqlPaginationSql(String originalSql, Page<?> page) {
int offset = (page.getPageNum() - 1) * page.getPageSize();
return originalSql + " LIMIT " + offset + ", " + page.getPageSize();
}
/**
* 统计总记录数
*/
private long countTotalRecords(String originalSql, Invocation invocation, MetaObject metaObject) throws Throwable {
// 构建 count SQL
String countSql = "SELECT COUNT(1) FROM (" + originalSql + ") AS temp_table";
// 此处简化,实际可通过 Connection 执行该 SQL 获取总条数
System.out.println("Count SQL: " + countSql);
return 100L; // 模拟总记录数
}
/**
* 生成代理对象
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 设置插件属性(配置文件中传入的参数)
*/
@Override
public void setProperties(Properties properties) {
// 可读取配置属性,如方言类型等
String dialect = properties.getProperty("dialect");
System.out.println("分页插件方言:" + dialect);
}
}十二、配置自动装配入口
在 src/main/resources/META-INF/spring/ 目录下创建文件 org.springframework.boot.autoconfigure.AutoConfiguration.imports,内容为自动配置类的全类名(Spring Boot 会自动扫描该文件,实现自动装配)
com.customer.mp.autoconfigure.MyBatisGenericAutoConfiguration以上步骤完成后,可以使用maven命令:mvn clean package打包,会看到在自己设置的repository里面可以找到新生成的jar。比如我的:
在D:\installed\apache-maven-3.9.11\repository\com\customer\mp\customer-mp\1.0.0目录下有customer-mp-1.0.0.jar
十三、使用
现在咱们已经实现了自定义starter,那如何使用呢?通常情况下,企业中一版是将自己的starter放在自己的仓库,然后配置好pom的仓库,就可以直接使用了。咱们本次使用的使用本地的,原理差不多。
1、新建springboot项目
我是新建的maven项目,然后慢慢修改的
pom文件:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.customer.starter</groupId>
<artifactId>customer-starter-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>customer-starter-demo</name>
<url>http://maven.apache.org</url>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.12.RELEASE</spring-boot.version>
</properties>
<repositories>
<!-- 配置本地仓库(优先级最高) -->
<repository>
<id>local-maven-repo</id>
<name>Local Maven Repository</name>
<url>D:\installed\apache-maven-3.9.11\repository</url> <!-- 指向本地仓库路径 -->
<releases>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy> <!-- 不更新本地已存在的包 -->
</releases>
<snapshots>
<enabled>false</enabled> <!-- 禁用快照版本(你的 Starter 是 release 1.0.0) -->
</snapshots>
</repository>
</repositories>
<dependencies>
<!-- Spring Boot Web 启动器(包含Web相关依赖) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot 核心启动器(基础依赖) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot JDBC 启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MyBatis Spring Boot 启动器(自动配置) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version> <!-- 与Spring Boot 2.3.12.RELEASE兼容 -->
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.customer.mp</groupId>
<artifactId>customer-mp</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 反射工具(简化开发) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
需要注意的是:
<dependency>
<groupId>com.customer.mp</groupId>
<artifactId>customer-mp</artifactId>
<version>1.0.0</version>
</dependency>这个就是咱们自己的starter的依赖,直接这样写是加载不到的,需要在添加一个配置:
<repositories>
<!-- 配置本地仓库(优先级最高) -->
<repository>
<id>local-maven-repo</id>
<name>Local Maven Repository</name>
<url>D:\installed\apache-maven-3.9.11\repository</url> <!-- 指向本地仓库路径 -->
<releases>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy> <!-- 不更新本地已存在的包 -->
</releases>
<snapshots>
<enabled>false</enabled> <!-- 禁用快照版本(你的 Starter 是 release 1.0.0) -->
</snapshots>
</repository>
</repositories>这个和设置私服地址类似,只时这里优先加载的是本地。这样就可以直接使用了。下面请看demo代码。
2.application.yml
server:
port: 8080
spring:
application:
name: customer-mp
profiles:
active: devspring:
datasource:
url: jdbc:mysql://xxxx.xxxx.xxxx.xxxx:3306/assistant?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useSSL=false
username: root
password: xxxxxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据源类型(默认 HikariCP,Spring Boot 自带,无需额外引入)
type: com.zaxxer.hikari.HikariDataSource # 兼容 JDK 1.8 的 HikariCP 版本
hikari:
maximum-pool-size: 8
minimum-idle: 2
connection-timeout: 30000
# 可选:配置 MyBatis 映射文件路径(若有自定义 XML Mapper)
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.customer.mp.entity3.mybatis配置
package com.customer.starter.config;
import com.customer.mp.base.interceptor.GenericSqlStatementHandlerInterceptor;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
@MapperScan(basePackages = "com.customer.mp.mapper") // 确保扫描到 UserMapper
public class MyBatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
// 注册通用拦截器(核心:仅需注册一次,适配所有Mapper)
GenericSqlStatementHandlerInterceptor genericInterceptor = new GenericSqlStatementHandlerInterceptor();
sqlSessionFactoryBean.setPlugins(new GenericSqlStatementHandlerInterceptor[]{genericInterceptor});
return sqlSessionFactoryBean;
}
}4.SysUserDO实体对象
package com.customer.starter.entity;
import com.customer.mp.base.anotation.TableId;
import com.customer.mp.base.anotation.TableName;
import com.customer.mp.base.enums.IdType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class SysUserDO {
@TableId(type = IdType.AUTO)
private Long id;
private String account;
private String username;
private String password;
private String phone;
private String email;
private Integer status;
private String createTime;
}5.UserMapper
package com.customer.starter.mapper;
import com.customer.mp.base.mapper.BaseMapper;
import com.customer.starter.entity.SysUserDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<SysUserDO> {
}6.service/impl
package com.customer.starter.service;
import com.customer.starter.entity.SysUserDO;
import java.util.List;
public interface UserService {
List<SysUserDO> list();
}
package com.customer.starter.service.impl;
import com.customer.starter.entity.SysUserDO;
import com.customer.starter.mapper.UserMapper;
import com.customer.starter.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public List<SysUserDO> list() {
return userMapper.selectList();
}
}7.控制器
package com.customer.starter.controller;
import com.customer.starter.entity.SysUserDO;
import com.customer.starter.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/list")
public List<SysUserDO> list() {
return userService.list();
}
}