在日常工作中,我们经常会遇到很多改数据与sql的优化或异常的情况,改数据的大多数可以编写sql来实现,但是小部分,需要通过现有逻辑的代码才行,这就需要发版迭代出一个改数据的接口;另一种就是线上sql的优化或调整,也只能通过发版来解决;由此我提出了“探针”方式,可以让我们探入系统中由于使用双手一样的丝滑。
第一道探针比较简单,实现思路是通过 BeanFacroty
来获取对应的 Bean
信息,然后结合反射,调用想要执行的方法,通过 controller
接口进行触发,以 Bean
、Method
的信息作为入参
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;
}
}
在工作中经常遇到访问慢的情况,需要优化,或者生产中由于某个条件写错导致sql执行异常等问题,此探针就是为解决此类问题而生。
整体实现思路是结合 Nacos 的动态配置,与 Mapper 的方法全限名结合,在 Mybatis
的拦截器中动态替换 MapperStatement
中的执行语句。
拦截器的代码逻辑实现如下
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;
}
}
}
通过 ApplicationListener<EnvironmentChangeEvent>
监听配置变更事件,每次刷新本地动态SQL缓存,提供查询性能,具体实现如下
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 配置属性如下
javadynamicSql:
# 格式: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 许可协议。转载请注明出处!