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 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的情况作了处理
* * @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的拦截器也是挺有用的!

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

你可能感兴趣的:(SpringBoot,SpringCloud,sql,java,数据库)