2025-05-30
实战设计
0

目录

任意调用 Bean 的方法
基于 Nacos 动态优化SQL
拦截器的实现
Nacos 配置刷新实现
安全性问题

在日常工作中,我们经常会遇到很多改数据与sql的优化或异常的情况,改数据的大多数可以编写sql来实现,但是小部分,需要通过现有逻辑的代码才行,这就需要发版迭代出一个改数据的接口;另一种就是线上sql的优化或调整,也只能通过发版来解决;由此我提出了“探针”方式,可以让我们探入系统中由于使用双手一样的丝滑。

任意调用 Bean 的方法

第一道探针比较简单,实现思路是通过 BeanFacroty 来获取对应的 Bean 信息,然后结合反射,调用想要执行的方法,通过 controller 接口进行触发,以 BeanMethod 的信息作为入参

java
@Data public class ProbeDTO { /** * bean名称 */ private String beanName; /** * 类名 */ private String className; /** * 方法名 */ private String methodName; /** * 参数 */ private Object[] params; }

Controller 接口中通过实现 BeanFactoryAware 来获取到 BeanFactory ,由于使用数组接收参数,实际调用反射的过程中,还需要将参数转为对应 Method 的入参类型,由于不同API的Bean转换与反射调用各有差异,出现过各种异常,所以此处兼容不同转换与反射的api调用,总有一种可用的方式

java
@RestController public class ProbeController implements BeanFactoryAware { private BeanFactory beanFactory; @PostMapping("/probe") public Object probe(@RequestBody ProbeDTO probeDTO) { try { // 类信息 Class<?> clazz = Class.forName(probeDTO.getClassName()); // 获取bean Object bean = beanFactory.getBean(probeDTO.getBeanName(), clazz); Assert.notNull(bean, "bean is null"); // 通过反射调用方法 Method method = ReflectUtil.getMethodByName(clazz, probeDTO.getMethodName()); Assert.notNull(method, "method is null"); Object[] params = new Object[ArrayUtil.isEmpty(probeDTO.getParams()) ? 0 : probeDTO.getParams().length]; if (ArrayUtil.isNotEmpty(method.getParameterTypes())) { for (int i = 0; i < method.getParameterTypes().length; i++) { // 基础类型 - 无需处理 if (method.getParameterTypes()[i].isPrimitive()) { params[i] = probeDTO.getParams()[i]; continue; } // 字符串类型 - 无需处理 if (method.getParameterTypes()[i].getName().equals(String.class.getName())) { params[i] = probeDTO.getParams()[i]; continue; } // 其他类型,list、map、set、bean try { // 类型转换器 SimpleTypeConverter simpleTypeConverter = new SimpleTypeConverter(); params[i] = simpleTypeConverter.convertIfNecessary(probeDTO.getParams()[i].toString(), method.getParameterTypes()[i]); } catch (Exception e) { try { // Hutool 转换 params[i] = JSONUtil.toBean(JSONUtil.toJsonStr(probeDTO.getParams()[i]), method.getParameterTypes()[i]); } catch (Exception e1) { try { // Fastjson 转换 params[i] = JSONObject.parseObject(probeDTO.getParams()[i].toString(), method.getParameterTypes()[i]); } catch (Exception e2) { // 转换不了,听天由命 params[i] = probeDTO.getParams()[i]; } } } } } try { // Hutool 反射调用,有时候不好用 return ReflectUtil.invoke(bean, method, params); } catch (Exception e) { e.printStackTrace(); // Spring 反射调用 return ReflectionUtils.invokeMethod(method, bean, params); } } catch (Exception e) { e.printStackTrace(); return e.getMessage(); } } /** * Callback that supplies the owning factory to a bean instance. * <p>Invoked after the population of normal bean properties * but before an initialization callback such as * {@link InitializingBean#afterPropertiesSet()} or a custom init-method. * * @param beanFactory owning BeanFactory (never {@code null}). * The bean can immediately call methods on the factory. * @throws BeansException in case of initialization errors * @see BeanInitializationException */ @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } }

基于 Nacos 动态优化SQL

在工作中经常遇到访问慢的情况,需要优化,或者生产中由于某个条件写错导致sql执行异常等问题,此探针就是为解决此类问题而生。

整体实现思路是结合 Nacos 的动态配置,与 Mapper 的方法全限名结合,在 Mybatis 的拦截器中动态替换 MapperStatement 中的执行语句。

拦截器的实现

拦截器的代码逻辑实现如下

  • 拦截 Executor 的 query 和 update 方法。
  • 根据 statementId(如 com.example.UserMapper.selectById)匹配动态 SQL。
  • 通过反射创建新的 MappedStatement,确保参数映射一致。
java
@Intercepts({ @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class} ), @Signature( type = Executor.class, method = "update", args = {MappedStatement.class, Object.class} ), }) @Slf4j @Component public class DynamicSqlInterceptor implements Interceptor { @Resource private NacosDynamicSqlLoader nacosDynamicSqlLoader; @Override public Object intercept(Invocation invocation) throws Throwable { // 参数 Object[] args = invocation.getArgs(); // 映射语句 MappedStatement mappedStatement = (MappedStatement) args[0]; // 查询参数 Object parameter = args[1]; // 执行器 Executor executor = (Executor) invocation.getTarget(); // 映射语句Id String statementId = mappedStatement.getId(); // 获取原始 BoundSql BoundSql originalBoundSql = null; if (args.length == 6) { // query 方法 originalBoundSql = (BoundSql) args[5]; } else if (args.length == 2) { // update 方法 originalBoundSql = mappedStatement.getBoundSql(parameter); } else { return invocation.proceed(); } log.info("{}:原始 SQL:{}", statementId, originalBoundSql.getSql()); // 获取动态 SQL String dynamicSql = nacosDynamicSqlLoader.getDynamicSql(statementId); log.info("{}:动态 SQL:{}", statementId, dynamicSql); if (dynamicSql != null && !dynamicSql.isEmpty()) { // 1. 创建新的 BoundSql(完整复制参数) BoundSql newBoundSql = new BoundSql( mappedStatement.getConfiguration(), dynamicSql, originalBoundSql.getParameterMappings(), originalBoundSql.getParameterObject() ); // 2. 复制附加参数(关键!) if (originalBoundSql.getAdditionalParameters() != null) { for (Map.Entry<String, Object> entry : originalBoundSql.getAdditionalParameters().entrySet()) { newBoundSql.setAdditionalParameter(entry.getKey(), entry.getValue()); } } // 3. 创建新的 MappedStatement MappedStatement newMappedStatement = copyMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql)); args[0] = newMappedStatement; // 清除一级二级缓存标记 boolean cleared = nacosDynamicSqlLoader.isCleared(statementId); // 4. 处理查询方法参数 if (args.length == 6) { // 更新 BoundSql 参数 args[5] = newBoundSql; // 5. 重新生成 CacheKey(关键!) RowBounds rowBounds = (RowBounds) args[2]; CacheKey newCacheKey = executor.createCacheKey( newMappedStatement, parameter, rowBounds, newBoundSql ); // 更新 CacheKey 参数 args[4] = newCacheKey; // 6. 清除一级缓存 if (!cleared) { executor.clearLocalCache(); } } // 7. 清除二级缓存 if (!cleared) { clearMapperCache(mappedStatement); nacosDynamicSqlLoader.clearDynamicSql(statementId); } } return invocation.proceed(); } /** * 清除二级缓存 */ private void clearMapperCache(MappedStatement ms) { try { Cache cache = ms.getCache(); if (cache != null) { cache.clear(); } } catch (Exception e) { // 避免因缓存异常导致主流程失败 } } /** * 复制并替换 MappedStatement 的 BoundSql */ private MappedStatement copyMappedStatement(MappedStatement ms, BoundSqlSqlSource boundSql) { try { // 通过反射创建新的 MappedStatement MappedStatement.Builder builder = new MappedStatement.Builder( ms.getConfiguration(), ms.getId(), boundSql, ms.getSqlCommandType() ); builder.resource(ms.getResource()) .fetchSize(ms.getFetchSize()) .statementType(ms.getStatementType()) .keyGenerator(ms.getKeyGenerator()) .timeout(ms.getTimeout()) .parameterMap(ms.getParameterMap()) .resultMaps(ms.getResultMaps()) .cache(ms.getCache()) .flushCacheRequired(ms.isFlushCacheRequired()) .useCache(ms.isUseCache()); // 处理主键生成(可选) if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) { builder.keyProperty(String.join(",", ms.getKeyProperties())); } return builder.build(); } catch (Exception e) { log.error("{}:动态SQL拦截器复制 MappedStatement 失败", ms.getId(), e); return ms; } } /** * 自定义 SqlSource 包装 BoundSql */ static class BoundSqlSqlSource implements SqlSource { private final BoundSql boundSql; public BoundSqlSqlSource(BoundSql boundSql) { this.boundSql = boundSql; } @Override public BoundSql getBoundSql(Object parameterObject) { return boundSql; } } }

Nacos 配置刷新实现

通过 ApplicationListener<EnvironmentChangeEvent> 监听配置变更事件,每次刷新本地动态SQL缓存,提供查询性能,具体实现如下

  • 动态 SQL 缓存在内存中,避免每次执行 SQL 时访问 Nacos。
  • 注意清除Mybatis的一级缓存、二级缓存。
java
@Component public class NacosDynamicSqlLoader implements EnvironmentAware, ApplicationListener<EnvironmentChangeEvent>, ApplicationRunner { /** * Nacos配置 - 动态SQL前缀 */ public static final String DYNAMIC_SQL_PREFIX = "dynamicSql."; private Environment environment; /** * 动态SQL缓存 */ private final Map<String, String> DYNAMIC_SQL_CACHE = new ConcurrentHashMap<>(); /** * 动态SQL缓存已清除标记 */ private final Map<String, Boolean> DYNAMIC_SQL_CLEAR_FLAG = new ConcurrentHashMap<>(); public String getDynamicSql(String statementId) { return DYNAMIC_SQL_CACHE.get(statementId); } /** * 是否已清除 */ public boolean isCleared(String statementId) { return DYNAMIC_SQL_CLEAR_FLAG.getOrDefault(statementId, false); } /** * 添加已清除标记 */ public void clearDynamicSql(String statementId) { DYNAMIC_SQL_CLEAR_FLAG.put(statementId, true); } /** * Handle an application event. * * @param event the event to respond to */ @Override public void onApplicationEvent(EnvironmentChangeEvent event) { // 监听到配置变更事件 loadConfig(); } /** * Set the {@code Environment} that this component runs in. * * @param environment */ @Override public void setEnvironment(Environment environment) { this.environment = environment; } /** * Callback used to run the bean. * * @param args incoming application arguments * @throws Exception on error */ @Override public void run(ApplicationArguments args) throws Exception { // 项目启动完成后加载配置 loadConfig(); } /** * 加载Nacos配置,更新动态SQL缓存 * * <p> * 项目启动时加载配置 * 配置变更时加载配置 * </p> */ private void loadConfig() { // 获取本地的配置 for (PropertySource<?> propertySource : ((ConfigurableEnvironment) environment).getPropertySources()) { if (propertySource instanceof BootstrapPropertySource<?>) { BootstrapPropertySource<?> bootstrapPropertySource = (BootstrapPropertySource<?>) propertySource; if (((Map<?, ?>) bootstrapPropertySource.getSource()).isEmpty()) { continue; } // 更新动态SQL parseAndUpdateCache((LinkedHashMap<String, Object>) bootstrapPropertySource.getSource()); } } } /** * 解析配置内容,更新动态SQL缓存 * * <p> * dynamicSql: * # 格式:Mapper接口全类名.方法名 * com.example.UserMapper.selectById: "SELECT * FROM user WHERE id = #{id} AND status = 1" * </p> * @param contextMap 配置内容,key: dynamicSql.com.example.UserMapper.selectById, value: SELECT * FROM user WHERE id = #{id} AND status = 1 */ private void parseAndUpdateCache(LinkedHashMap<String, Object> contextMap) { // 清空缓存,防止直接put存留被删除的sql DYNAMIC_SQL_CACHE.clear(); DYNAMIC_SQL_CLEAR_FLAG.clear(); // 添加缓存 if (contextMap == null || contextMap.isEmpty()) { return; } contextMap.forEach((key, value) -> { if (key.startsWith(DYNAMIC_SQL_PREFIX)) { String id = key.replace(DYNAMIC_SQL_PREFIX, ""); DYNAMIC_SQL_CACHE.put(id, value.toString()); DYNAMIC_SQL_CLEAR_FLAG.put(id, false); } }); } }

Nacos 配置属性如下

java
dynamicSql: # 格式:Mapper接口全类名.方法名 cn.liushigong.system.mapper.UsersMapper.selectList: "SELECT * FROM users WHERE age = ? or age= ?" cn.liushigong.system.mapper.UsersMapper.selectByUser: "SELECT * FROM users WHERE name = ? and age = ? and sex = ?"

日志输出

java
原始 SQL:SELECT id,name,age,sex,create_time FROM users WHERE (age > ? AND age >= ?) 动态 SQL:SELECT * FROM users WHERE age = ? or age= ? c.l.i.c.s.mapper.UsersMapper.selectList : ==> Preparing: SELECT * FROM users WHERE age = ? or age= ? c.l.i.c.s.mapper.UsersMapper.selectList : ==> Parameters: 10(Integer), 50(Integer) 原始 SQL:select * from users WHERE name like concat('%',?,'%') and age = ? and sex = ? 动态 SQL:SELECT * FROM users WHERE name = ? and age = ? and sex = ? c.l.i.c.s.m.UsersMapper.selectByUser : ==> Preparing: SELECT * FROM users WHERE name = ? and age = ? and sex = ? c.l.i.c.s.m.UsersMapper.selectByUser : ==> Parameters: 张三(String), 18(Integer), 男(String)

动态SQL注意项

原SQL会解析占位符 ?,每个占位符对应的参数都是固定的,在调整动态SQL的过程中,需要保持占位符 ?的数量、查询参数的顺序与原SQL保持一致

安全性问题

探针虽然带来了一些遍历,但同时要注意其产生的安全性问题,例如任意调用Bean信息,没有记录留痕,没有traceId 的记录,无法定位到 操作凶手,随意更改sql,导致系统的安全性与稳定性无法得到保障。

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!