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的拦截器也是挺有用的!

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

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Mybatis-plus是Mybatis的增强工具,它提供了许多方便的功能,方便开发人员进行Mybatis的使用。在实际开发过程中,我们经常需要打印SQL语句以及填充参数,以便于我们更好地调试和优化SQL语句。 在Mybatis-plus中,打印SQL语句以及填充参数非常简单。我们只需要在配置文件中添加一个配置,就可以实现打印SQL语句和填充参数的功能。具体步骤如下: 1.在配置文件中添加配置项 在Mybatis的配置文件中,添加如下配置项: ```xml <!-- 打印SQL语句 --> <setting name="logImpl" value="STDOUT_LOGGING"/> <!-- 显示SQL语句中的参数 --> <setting name="jdbcTypeForNull" value="NULL"/> <setting name="logLevel" value="DEBUG"/> ``` 2.在类中使用注解 在我们需要打印SQL语句的Mapper中,使用注解@Mapper以及@Log4j2即可。 ```java @Mapper @Log4j2 public interface UserMapper extends BaseMapper<User> { @Select("select * from user WHERE name = #{name}") User queryUserByName(String name); } ``` 这样,当我们使用queryUserByName方法的时候,就会在控制台上打印出SQL语句以及填充的参数Mybatis-plus是一个非常方便的工具,使用起来也非常简单,只需要简单的配置和使用注解即可实现打印SQL语句以及填充参数的功能,方便我们进行SQL优化和调试。 ### 回答2: Mybatisplus是一个基于Mybatis框架之上的底层扩展库,提供了很多便于开发的功能,如自动生成代码、分页插件、乐观锁、多租户插件等。同时,它也提供了打印SQL语句和填充参数的功能。下面就针对这两个功能进行讲解。 一、打印SQL语句 Mybatisplus提供了打印SQL语句的功能,可以在SQL执行前和执行后打印SQL语句,方便开发人员查看和排错。使用方式如下: 1.在配置文件中设置打印SQL语句的日志级别为DEBUG: ``` logging.level.com.baomidou.mybatisplus.core.executor=DEBUG ``` 2.对于需要打印SQL语句的Mapper方法,可以在方法上加上注解@Interceptor(ExecuteSqlInterceptor.class): ``` @Interceptor(ExecuteSqlInterceptor.class) List<User> selectUserList(); ``` 3.执行SQL语句后,在控制台中可以看到打印出来的SQL语句,例如: ``` DEBUG 9307 --- [nio-8080-exec-1] c.b.m.c.e.MPExecuteSqlInterceptor : ==> Preparing: SELECT * FROM user WHERE age > ? DEBUG 9307 --- [nio-8080-exec-1] c.b.m.c.e.MPExecuteSqlInterceptor : ==> Parameters: 18(Integer) DEBUG 9307 --- [nio-8080-exec-1] c.b.m.c.e.MPExecuteSqlInterceptor : <== Total: 2 ``` 二、填充参数 Mybatisplus还提供了填充参数的功能,可以在执行SQL语句前和执行后对参数进行处理。使用方式如下: 1.创建一个继承了HandlerInterceptor接口的类: ``` public class MybatisParamHandlerInterceptor implements HandlerInterceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 获取参数 Object[] args = invocation.getArgs(); // 处理参数 // ... // 执行方法 Object result = invocation.proceed(); // 处理返回值 // ... return result; } } ``` 2.在配置文件中配置自定义的参数处理器: ``` mybatis-plus: configuration: # 自定义参数拦截器 handler: MybatisParamHandlerInterceptor: com.example.handler.MybatisParamHandlerInterceptor ``` 3.对于需要自定义处理参数的Mapper方法,可以在方法上加上注解@Interceptor(MybatisParamHandlerInterceptor.class ): ``` @Interceptor(MybatisParamHandlerInterceptor.class ) List<User> selectUserList(int age, String name); ``` 4.执行SQL语句前,Mybatisplus会调用参数处理器中的intercept方法,可以进行参数处理。例如: ``` @Override public Object intercept(Invocation invocation) throws Throwable { // 获取参数 Object[] args = invocation.getArgs(); // 处理参数 args[0] = args[0] + 1; // 执行方法 Object result = invocation.proceed(); // 处理返回值 // ... return result; } ``` 通过以上方法,我们可以轻松地实现打印SQL语句和填充参数的功能,提高开发效率和调试效率。 ### 回答3: MybatisPlus 是一个基于 Mybatis 的增强工具包,为了在开发过程中更加方便的进行 SQL 语句的构建和执行,它提供了很多方便的功能。其中有一项非常实用的功能就是可以打印 SQL 语句并填充参数。下面我就来详细介绍一下如何使用 MybatisPlus 打印 SQL 语句并填充参数。 1. 配置 mybatis-plus 配置文件mybatis-plus 配置文件中,我们需要打开 ShowSql 开关和 FormatSql 开关。如下: ``` # 显示sql mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl sql-injector: com.baomidou.mybatisplus.core.injector.LogicSqlInjector tenant-handler: xxx.TenantHandler db-config: logic-delete-field: DELETED query-strategy: # 查询全部(-1),单条(0),分页(1) id-type: uuid field-strategy: # 必须写,否则无法生效,开启下划线转驼峰命名 - com.baomidou.mybatisplus.generator.config.po.TableField type-enums-package: - com.xxx block-attack-enable: false table-prefix: - xxx_ schema: - cxdatabase - aadatabase - xxxdatabase Capital-Mode: false show-sql: true format-sql: true ``` 2. 查看打印日志 开启后直接查看控制台日志即可看到 SQL 语句和参数。 ``` 2021-03-29 19:42:06,144 INFO [http-nio-8080-exec-1] o.m.p.mapper.BaseMapper.selectList(BaseMapper.java:128) - ==> Preparing: SELECT id, name FROM user WHERE age > ? OR age < ? ORDER BY age DESC LIMIT ? 2021-03-29 19:42:06,149 INFO [http-nio-8080-exec-1] o.m.p.mapper.BaseMapper.selectList(BaseMapper.java:128) - ==> Parameters: 22(Integer), 35(Integer), 10(Integer) ``` 这里就可以看到完整SQL 语句和填充的参数了。 总之,使用 MybatisPlus 打印 SQL 语句并填充参数是非常实用的功能,能够在开发过程中方便我们调试 SQL 语句,定位问题,提高开发效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值