您现在的位置是:首页 >技术杂谈 >MybatisPLus输出sql语句到指定文件(附带完整的参数)网站首页技术杂谈

MybatisPLus输出sql语句到指定文件(附带完整的参数)

༺鲸落༻ 2023-07-08 04:00:03
简介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的拦截器也是挺有用的!

下一篇我将教教大家系统发生异常时,如何备份还没执行的请求体,以至系统恢复时重新执行入库

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。