您现在的位置是:首页 >技术杂谈 >MybatisPLus输出sql语句到指定文件(附带完整的参数)网站首页技术杂谈
MybatisPLus输出sql语句到指定文件(附带完整的参数)
简介MybatisPLus输出sql语句到指定文件(附带完整的参数)
需求
现有两个分布式的项目,其中,一个项目(项目A)主要提供保存sql语句到指定文件的服务,另一个项目就是常见的业务项目(项目B),里面包含一些增删改查操作,现想将业务处理时操作数据库的sql语句调用项目A的服务保存到指定文件中
思路
(1)由于两个分布式项目目前未注入到nacos中,故项目B需使用RestTemplate来调用项目B的服务;
(2)通过实现MybatisPLus的拦截器获取业务处理时操作数据库的sql语句
步骤
1、项目A编写保存sql语句到指定文件路径的服务
(1)保存sql到指定文件路径的工具类
import lombok.SneakyThrows;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
public class FileUtil {
@SneakyThrows
public static void insertSqlToFile(String context, String filePath){
//创建文件
File file = new File(filePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
if(!file.exists()) {
file.createNewFile();
}
//写入文件
FileWriter fw = new FileWriter(filePath, true);
BufferedWriter bw = new BufferedWriter(fw);
bw.write(context);
bw.newLine();
bw.close();
fw.close();
}
}
(2)向外保存提供sql语句到文件中的服务
import com.cspg.backup.model.FileRequest;
import com.cspg.backup.util.FileUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class BackupController {
@SneakyThrows
@PostMapping("/pushSqlToFile")
@ApiOperation("将未执行的请求内容插入到文件中")
public void pushSqlToFile(@RequestBody FileRequest fileRequest){
FileUtils.insertSqlToFile(fileRequest);
}
}
2、项目B通过实现MybatisPLus的拦截器获取业务处理时操作数据库的sql语句
(1)编写RestTemplate配置类注册RestTemplate到spring容器中
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.TimeUnit;
/**
* @Author ning shang xu
* @Date 2023/4/1 下午2:34
**/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(){
OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS) // default 10s
.writeTimeout(60, TimeUnit.SECONDS) // default 10s
.readTimeout(60, TimeUnit.SECONDS) // default 10s
.connectionPool(new ConnectionPool(50,40,TimeUnit.HOURS))
.build();
OkHttp3ClientHttpRequestFactory requestFactory = new OkHttp3ClientHttpRequestFactory(httpClient);
//使用OkHttp内置的HTTP连接池提高RestTemplate的访问速率
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}
}
(2)实现MybatisPLus的拦截器获取业务处理时操作数据库的sql语句,并调用项目A提供的服务
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import okhttp3.MediaType;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.Statement;
import java.text.DateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
/**
* 拦截StatementHandler类中参数类型为Statement的 prepare 方法
* 即拦截 Statement prepare(Connection var1, Integer var2) 方法
*/
@Component
@Slf4j
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class MybatisSqlLoggerInterceptor implements Interceptor {
private static final String SQL_BACKUP_URL="http://ip:port/api/sqlBackUp/pushSqlToFile";
@Autowired
private RestTemplate restTemplate;
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
//通过MetaObject优雅访问对象的属性,这里是访问statementHandler的属性;:MetaObject是Mybatis提供的一个用于方便、
//优雅访问对象属性的对象,通过它可以简化代码、不需要try/catch各种reflect异常,同时它支持对JavaBean、Collection、Map三种类型对象的操作。
MetaObject metaObject = MetaObject
.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
new DefaultReflectorFactory());
//先拦截到RoutingStatementHandler,里面有个StatementHandler类型的delegate变量,其实现类是BaseStatementHandler,然后就到BaseStatementHandler的成员变量mappedStatement
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
//id为执行的mapper方法的全路径名,如com.uv.dao.UserMapper.insertUser
String id = mappedStatement.getId();
log.info("id ==> " + id);
//sql语句类型 select、delete、insert、update
String sqlCommandType = mappedStatement.getSqlCommandType().toString();
log.info("类型 ==> " + sqlCommandType);
BoundSql boundSql = statementHandler.getBoundSql();
//获取数据库名称
SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) SpringContextUtil.applicationContext.getBean("sqlSessionFactory");
Connection connection = sqlSessionFactory.openSession().getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT DATABASE()");
resultSet.next();
String databaseName = resultSet.getString(1);
resultSet.close();
statement.close();
connection.close();
log.info("数据库名称 ==>: " + databaseName);
// 获取节点的配置
Configuration configuration = mappedStatement.getConfiguration();
// 获取到最终的sql语句
String newsql = getSql(configuration, boundSql, id);
log.info("拦截的sql ==>: " + newsql);
long start = System.currentTimeMillis();
Object returnValue = invocation.proceed();
long end = System.currentTimeMillis();
long time = (end - start);
log.info("sql耗时 ==>: " + time);
String filePath = "/data/home/nsx/mm/" + databaseName + ".sql";
log.info("开始请求第三方保存sql语句到本地文件中");
/* ResponseEntity<String> responseEntity = restTemplate.exchange(SQL_BACKUP_URL, HttpMethod.POST, null, String.class, newsql, filePath);
log.info("responseEntity --- 是"+ responseEntity);*/
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS) // default 10s
.writeTimeout(60, TimeUnit.SECONDS) // default 10s
.readTimeout(60, TimeUnit.SECONDS) // default 10s
.connectionPool(new ConnectionPool(20,15,TimeUnit.HOURS))
.build();
// 构建请求体
MediaType JSON = MediaType.parse("application/json; charset=utf-8");
JSONObject json = JSONUtil.createObj();
json.put("content",newsql);
json.put("filePath", filePath);
RequestBody requestBody = RequestBody.create(JSON, json.toString());
Request request = new Request.Builder()
.url(SQL_BACKUP_URL)
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
String responseBody = response.body().string();
System.out.println(responseBody);
}
log.info("开始请求第三方保存sql语句到本地文件中");
HashMap<String, Object> requestBody = new HashMap<>();
requestBody.put("content",newsql);
requestBody.put("filePath",filePath);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<HashMap<String, Object>> requestEntity = new HttpEntity<>(requestBody, httpHeaders);
ResponseEntity<String> responseEntity = restTemplate.exchange(SQL_BACKUP_URL, HttpMethod.POST, requestEntity, String.class);
log.info("responseEntity --- 是"+ responseEntity);
return returnValue;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
/**
* 封装了一下sql语句,
* 使得结果返回完整xml路径下的sql语句节点id + sql语句
*
* @param configuration
* @param boundSql
* @param sqlId
* @return
*/
private String getSql(Configuration configuration, BoundSql boundSql, String sqlId) {
String sql = showSql(configuration, boundSql);
StringBuilder str = new StringBuilder(100);
// str.append(sqlId);
// str.append(":");
str.append(sql);
str.append(";");
return str.toString();
}
/**
* 如果参数是String,则添加单引号, 如果是日期,则转换为时间格式器并加单引号;
* 对参数是null和不是null的情况作了处理<br>
*
* @param obj
* @return
*/
private String getParameterValue(Object obj) {
String value = null;
if (obj instanceof String) {
value = "'" + obj.toString() + "'";
} else if (obj instanceof Date) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
value = "'" + formatter.format(new Date()) + "'";
} else {
if (obj != null) {
value = obj.toString();
} else {
value = "";
}
}
return value;
}
/**
* 进行?的替换
* @param configuration
* @param boundSql
* @return
*/
public String showSql(Configuration configuration, BoundSql boundSql) {
// 获取参数
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql
.getParameterMappings();
// sql语句中多个空格都用一个空格代替
String sql = boundSql.getSql().replaceAll("[\s]+", " ");
if (CollectionUtils.isNotEmpty(parameterMappings) && parameterObject != null) {
// 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换
// 如果根据parameterObject.getClass()可以找到对应的类型,则替换
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\?", Matcher.quoteReplacement(getParameterValue(parameterObject)));
} else {
//MetaObject主要是封装了originalObject对象,
// 提供了get和set的方法用于获取和设置originalObject的属性值,
// 主要支持对JavaBean、Collection、Map三种类型对象的操作
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\?", Matcher.quoteReplacement(getParameterValue(obj)));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
// 该分支是动态sql
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\?", Matcher.quoteReplacement(getParameterValue(obj)));
} else {
//打印出缺失,提醒该参数缺失并防止错位
sql = sql.replaceFirst("\?", "缺失");
}
}
}
}
return sql;
}
}
mybatis-plus在配置文件里是这样的,不用加 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus:
mapper-locations: classpath*:/mapper/iot/*.xml,classpath*:/mapper/*.xml
global-config:
db-config:
property-format: ""%s""
总结
其实实际需求,是想发生异常时将sql语句保存到文件中,但是当发生异常时mybatis在执行的过程已经报错了,不走拦截器里的方法了,所以这个思路不可行,但大家不考虑异常的情况下,学学mybatis的拦截器也是挺有用的!
下一篇我将教教大家系统发生异常时,如何备份还没执行的请求体,以至系统恢复时重新执行入库
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。