百度真题:MyBatis插件开发实战

面试重要程度:⭐⭐⭐⭐⭐

真题来源:百度2024秋招技术面试

考察重点:插件机制原理、拦截器开发、实际应用场景

预计阅读时间:35分钟

真题背景

面试官: "我看你简历上写熟悉MyBatis,那你能不能给我们开发一个MyBatis插件,用来监控SQL执行时间,并且对慢SQL进行告警?请详细说说插件的实现原理和具体代码。"

考察意图:

  • 对MyBatis插件机制的深度理解
  • 实际编码能力和问题解决能力
  • 对生产环境监控的实践经验
  • 代码设计和架构思维

🎯 插件机制原理深度解析

拦截器工作原理

核心概念:

// MyBatis插件本质上是一个拦截器
// 基于JDK动态代理实现,可以拦截四大核心对象的方法调用

/**
 * 四大核心对象:
 * 1. Executor - 执行器,执行SQL语句
 * 2. StatementHandler - 语句处理器,处理SQL语句
 * 3. ParameterHandler - 参数处理器,处理SQL参数
 * 4. ResultSetHandler - 结果集处理器,处理查询结果
 */

// 插件接口定义
public interface Interceptor {
    
    // 拦截目标方法的调用
    Object intercept(Invocation invocation) throws Throwable;
    
    // 生成目标对象的代理
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    // 设置插件属性
    default void setProperties(Properties properties) {
        // 空实现
    }
}

🚀 SQL执行时间监控插件实现

核心监控插件

/**
 * SQL执行时间监控插件
 * 功能:
 * 1. 监控所有SQL执行时间
 * 2. 慢SQL告警(可配置阈值)
 * 3. SQL执行统计
 * 4. 异常SQL记录
 */
@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Component
@Slf4j
public class SqlExecutionTimeInterceptor implements Interceptor {
    
    // 慢SQL阈值(毫秒),默认1秒
    private long slowSqlThreshold = 1000;
    
    // 是否启用详细日志
    private boolean enableDetailLog = true;
    
    // SQL统计信息
    private final ConcurrentHashMap<String, SqlStatistics> sqlStatisticsMap = new ConcurrentHashMap<>();
    
    // 告警服务
    @Autowired(required = false)
    private AlertService alertService;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        String sqlId = null;
        String sql = null;
        Object parameter = null;
        
        try {
            // 获取SQL信息
            MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
            parameter = invocation.getArgs()[1];
            sqlId = mappedStatement.getId();
            
            // 获取实际执行的SQL
            BoundSql boundSql = mappedStatement.getBoundSql(parameter);
            sql = boundSql.getSql();
            
            if (enableDetailLog) {
                log.info("开始执行SQL - ID: {}, SQL: {}", sqlId, formatSql(sql));
            }
            
            // 执行原方法
            Object result = invocation.proceed();
            
            // 计算执行时间
            long endTime = System.currentTimeMillis();
            long executionTime = endTime - startTime;
            
            // 记录执行成功
            recordSqlExecution(sqlId, sql, parameter, executionTime, true, null);
            
            // 检查是否为慢SQL
            if (executionTime > slowSqlThreshold) {
                handleSlowSql(sqlId, sql, parameter, executionTime);
            }
            
            if (enableDetailLog) {
                log.info("SQL执行完成 - ID: {}, 耗时: {}ms", sqlId, executionTime);
            }
            
            return result;
            
        } catch (Exception e) {
            long endTime = System.currentTimeMillis();
            long executionTime = endTime - startTime;
            
            // 记录执行失败
            recordSqlExecution(sqlId, sql, parameter, executionTime, false, e);
            
            log.error("SQL执行异常 - ID: {}, 耗时: {}ms, SQL: {}, 异常: {}", 
                sqlId, executionTime, formatSql(sql), e.getMessage(), e);
            
            // 发送异常告警
            if (alertService != null) {
                alertService.sendSqlErrorAlert(sqlId, sql, executionTime, e);
            }
            
            throw e;
        }
    }
    
    /**
     * 处理慢SQL
     */
    private void handleSlowSql(String sqlId, String sql, Object parameter, long executionTime) {
        log.warn("慢SQL告警 - ID: {}, 耗时: {}ms, SQL: {}, 参数: {}", 
            sqlId, executionTime, formatSql(sql), formatParameter(parameter));
        
        // 发送慢SQL告警
        if (alertService != null) {
            alertService.sendSlowSqlAlert(sqlId, sql, parameter, executionTime);
        }
        
        // 记录慢SQL到数据库或文件
        recordSlowSql(sqlId, sql, parameter, executionTime);
    }
    
    /**
     * 记录SQL执行统计
     */
    private void recordSqlExecution(String sqlId, String sql, Object parameter, 
                                   long executionTime, boolean success, Exception exception) {
        sqlStatisticsMap.compute(sqlId, (key, statistics) -> {
            if (statistics == null) {
                statistics = new SqlStatistics(sqlId, sql);
            }
            
            statistics.addExecution(executionTime, success);
            
            if (!success && exception != null) {
                statistics.addException(exception);
            }
            
            return statistics;
        });
    }
    
    /**
     * 格式化SQL
     */
    private String formatSql(String sql) {
        if (sql == null) {
            return "";
        }
        return sql.replaceAll("\\s+", " ").trim();
    }
    
    /**
     * 格式化参数
     */
    private String formatParameter(Object parameter) {
        if (parameter == null) {
            return "null";
        }
        
        try {
            if (parameter instanceof String || parameter instanceof Number) {
                return parameter.toString();
            } else {
                // 使用JSON序列化复杂对象,但限制长度
                String json = JsonUtils.toJson(parameter);
                return json.length() > 500 ? json.substring(0, 500) + "..." : json;
            }
        } catch (Exception e) {
            return parameter.getClass().getSimpleName() + "@" + parameter.hashCode();
        }
    }
    
    @Override
    public Object plugin(Object target) {
        // 只拦截Executor类型的对象
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }
    
    @Override
    public void setProperties(Properties properties) {
        // 从配置文件读取参数
        String threshold = properties.getProperty("slowSqlThreshold");
        if (StringUtils.isNotBlank(threshold)) {
            this.slowSqlThreshold = Long.parseLong(threshold);
        }
        
        String enableDetail = properties.getProperty("enableDetailLog");
        if (StringUtils.isNotBlank(enableDetail)) {
            this.enableDetailLog = Boolean.parseBoolean(enableDetail);
        }
        
        log.info("SQL监控插件配置 - 慢SQL阈值: {}ms, 详细日志: {}", 
            slowSqlThreshold, enableDetailLog);
    }
}

统计信息数据结构

/**
 * SQL执行统计信息
 */
@Data
public class SqlStatistics {
    
    private String sqlId;
    private String sql;
    private long totalExecutions;      // 总执行次数
    private long successExecutions;    // 成功执行次数
    private long failedExecutions;     // 失败执行次数
    private long totalExecutionTime;   // 总执行时间
    private long minExecutionTime;     // 最小执行时间
    private long maxExecutionTime;     // 最大执行时间
    private double avgExecutionTime;   // 平均执行时间
    private LocalDateTime firstExecutionTime;  // 首次执行时间
    private LocalDateTime lastExecutionTime;   // 最后执行时间
    private List<String> recentExceptions;     // 最近的异常信息
    
    public SqlStatistics(String sqlId, String sql) {
        this.sqlId = sqlId;
        this.sql = sql;
        this.totalExecutions = 0;
        this.successExecutions = 0;
        this.failedExecutions = 0;
        this.totalExecutionTime = 0;
        this.minExecutionTime = Long.MAX_VALUE;
        this.maxExecutionTime = 0;
        this.recentExceptions = new ArrayList<>();
    }
    
    /**
     * 添加执行记录
     */
    public synchronized void addExecution(long executionTime, boolean success) {
        this.totalExecutions++;
        this.totalExecutionTime += executionTime;
        
        if (success) {
            this.successExecutions++;
        } else {
            this.failedExecutions++;
        }
        
        // 更新最小最大执行时间
        this.minExecutionTime = Math.min(this.minExecutionTime, executionTime);
        this.maxExecutionTime = Math.max(this.maxExecutionTime, executionTime);
        
        // 计算平均执行时间
        this.avgExecutionTime = (double) this.totalExecutionTime / this.totalExecutions;
        
        // 更新执行时间
        LocalDateTime now = LocalDateTime.now();
        if (this.firstExecutionTime == null) {
            this.firstExecutionTime = now;
        }
        this.lastExecutionTime = now;
    }
    
    /**
     * 添加异常信息
     */
    public synchronized void addException(Exception exception) {
        String exceptionInfo = exception.getClass().getSimpleName() + ": " + exception.getMessage();
        
        // 只保留最近10个异常
        if (recentExceptions.size() >= 10) {
            recentExceptions.remove(0);
        }
        recentExceptions.add(exceptionInfo);
    }
    
    /**
     * 获取成功率
     */
    public double getSuccessRate() {
        if (totalExecutions == 0) {
            return 0.0;
        }
        return (double) successExecutions / totalExecutions * 100;
    }
}

告警服务实现

/**
 * SQL告警服务
 */
public interface AlertService {
    
    /**
     * 发送慢SQL告警
     */
    void sendSlowSqlAlert(String sqlId, String sql, Object parameter, long executionTime);
    
    /**
     * 发送SQL异常告警
     */
    void sendSqlErrorAlert(String sqlId, String sql, long executionTime, Exception exception);
}

/**
 * 告警服务实现
 */
@Service
@Slf4j
public class AlertServiceImpl implements AlertService {
    
    @Autowired(required = false)
    private DingTalkService dingTalkService;
    
    @Value("${sql.monitor.alert.enabled:true}")
    private boolean alertEnabled;
    
    @Override
    public void sendSlowSqlAlert(String sqlId, String sql, Object parameter, long executionTime) {
        if (!alertEnabled) {
            return;
        }
        
        try {
            String message = buildSlowSqlAlertMessage(sqlId, sql, parameter, executionTime);
            
            // 发送钉钉告警
            if (dingTalkService != null) {
                dingTalkService.sendMessage("慢SQL告警", message);
            }
            
        } catch (Exception e) {
            log.error("发送慢SQL告警失败", e);
        }
    }
    
    @Override
    public void sendSqlErrorAlert(String sqlId, String sql, long executionTime, Exception exception) {
        if (!alertEnabled) {
            return;
        }
        
        try {
            String message = buildSqlErrorAlertMessage(sqlId, sql, executionTime, exception);
            
            if (dingTalkService != null) {
                dingTalkService.sendMessage("SQL异常告警", message);
            }
            
        } catch (Exception e) {
            log.error("发送SQL异常告警失败", e);
        }
    }
    
    private String buildSlowSqlAlertMessage(String sqlId, String sql, Object parameter, long executionTime) {
        StringBuilder sb = new StringBuilder();
        sb.append("🐌 慢SQL告警\n");
        sb.append("执行时间: ").append(executionTime).append("ms\n");
        sb.append("SQL ID: ").append(sqlId).append("\n");
        sb.append("SQL语句: ").append(sql).append("\n");
        sb.append("参数: ").append(parameter).append("\n");
        sb.append("时间: ").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        return sb.toString();
    }
    
    private String buildSqlErrorAlertMessage(String sqlId, String sql, long executionTime, Exception exception) {
        StringBuilder sb = new StringBuilder();
        sb.append("❌ SQL异常告警\n");
        sb.append("执行时间: ").append(executionTime).append("ms\n");
        sb.append("SQL ID: ").append(sqlId).append("\n");
        sb.append("SQL语句: ").append(sql).append("\n");
        sb.append("异常信息: ").append(exception.getMessage()).append("\n");
        sb.append("异常类型: ").append(exception.getClass().getSimpleName()).append("\n");
        sb.append("时间: ").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        return sb.toString();
    }
}

🔧 插件配置与注册

Spring Boot集成配置

@Configuration
public class MyBatisPluginConfig {
    
    @Bean
    public SqlExecutionTimeInterceptor sqlExecutionTimeInterceptor() {
        return new SqlExecutionTimeInterceptor();
    }
    
    @Bean
    public ConfigurationCustomizer configurationCustomizer(SqlExecutionTimeInterceptor sqlInterceptor) {
        return configuration -> {
            // 注册SQL监控插件
            configuration.addInterceptor(sqlInterceptor);
            
            // 设置插件属性
            Properties properties = new Properties();
            properties.setProperty("slowSqlThreshold", "1000");
            properties.setProperty("enableDetailLog", "true");
            sqlInterceptor.setProperties(properties);
        };
    }
}

监控管理接口

/**
 * SQL监控管理接口
 */
@RestController
@RequestMapping("/api/sql-monitor")
public class SqlMonitorController {
    
    @Autowired
    private SqlExecutionTimeInterceptor sqlInterceptor;
    
    @GetMapping("/statistics")
    public Result<Map<String, SqlStatistics>> getStatistics() {
        Map<String, SqlStatistics> statistics = sqlInterceptor.getSqlStatistics();
        return Result.success(statistics);
    }
    
    @GetMapping("/slow-sql")
    public Result<List<SqlStatistics>> getSlowSqlStatistics() {
        List<SqlStatistics> slowSqls = sqlInterceptor.getSlowSqlStatistics();
        return Result.success(slowSqls);
    }
    
    @PostMapping("/clear-statistics")
    public Result<Void> clearStatistics() {
        sqlInterceptor.clearStatistics();
        return Result.success();
    }
}

💡 面试回答要点

标准回答框架

第一部分:原理阐述

"MyBatis插件基于JDK动态代理实现,可以拦截四大核心对象的方法调用。
我设计的SQL监控插件主要拦截Executor的query和update方法,
通过@Intercepts注解指定拦截点,在intercept方法中实现监控逻辑。"

第二部分:技术实现

"插件的核心是记录SQL执行前后的时间戳,计算执行时间。
对于慢SQL,我设置了可配置的阈值,超过阈值就触发告警。
同时维护了一个ConcurrentHashMap来统计每个SQL的执行情况,
包括执行次数、平均耗时、成功率等指标。"

第三部分:生产应用

"在生产环境中,这个插件帮助我们及时发现性能问题。
通过钉钉告警,运维团队能够第一时间收到慢SQL通知。
统计功能让我们能够分析SQL性能趋势,指导数据库优化工作。"

扩展考点

1. 性能影响

面试官:"这个插件会不会影响系统性能?"

回答要点:
- 插件本身开销很小,主要是时间戳记录
- 使用异步方式发送告警,不阻塞主流程
- 统计信息使用内存缓存,定期清理
- 可以通过配置开关控制功能启用

2. 扩展功能

面试官:"还能增加什么功能?"

回答要点:
- SQL执行计划分析
- 参数绑定监控
- 连接池状态监控
- 自动SQL优化建议
- 与APM系统集成

本章核心要点:

  • ✅ MyBatis插件机制原理和四大核心对象
  • ✅ 完整的SQL监控插件实现
  • ✅ 统计信息收集和告警机制
  • ✅ Spring Boot集成配置
  • ✅ 生产环境应用和性能考虑

总结: MyBatis插件开发体现了对框架深度理解和实际问题解决能力,是高级Java工程师必备技能

#java秋招面试#
Java面试圣经 文章被收录于专栏

Java面试圣经

全部评论
坐标南京,OD岗位多多,欢迎私聊
点赞 回复 分享
发布于 08-23 15:39 贵州

相关推荐

码农顶针:估计让你免费辅导老板孩子的学习
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务