詹学伟
詹学伟
Published on 2025-12-31 / 13 Visits
0
0

SpringBoot2.x自定义starter

一、说明

咱们使用SpringBoot的时候,经常使用xxx-starter,比如:mybatis-spring-boot-starterspring-boot-starter等等,然后就可以再加载maven后,使用相应的对象。你有没有这样的疑问:为什么一个简单地引入一个starter就可以使用某个对象?今天我们来揭开这个面纱,并从零开始写一个自定义的starter,并且运用起来。介绍文章见另外一篇。

下面我就以我仿mybatis-plus的自定义starterdemo为例。

二、代码结构

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: dev
spring:
  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.entity

3.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();
    }
}


Comment