mybatis log sql 生成
虽然 idea 中默认有 sql 格式化功能,但是生产中有 sql debug 日志, 每次都要填充参数,每次都要格式化,非常麻烦.
使用 thymeleaf
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>sqlformatter</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 替换为你的主类全限定名,例如 com.example.Application -->
<mainClass>com.example.coop.BootCoopApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
接口实现
@RestController
public class SqlGenController {
@Autowired
private SqlGenService sqlGenService;
@PostMapping("/mybatis/sqllog/format")
public String genSql(@RequestBody LogDto dto) {
if (StrUtil.isBlank(dto.getData())) {
throw new RuntimeException("数据不能为空");
}
return sqlGenService.genSql(dto.getData());
}
}
//返回页面
@Controller
public class SqlFormatController {
@GetMapping("/sql-format")
public String index() {
return "mybatis-sql-formatter";
}
}
//service 实现
@Service
public class SqlGenService {
private static final Pattern PARAM_PATTERN = Pattern.compile("([^,]+?)\\s*\\(([^)]+)\\)");
// 常见换行符模式
public static final String PREPARING = "Preparing";
public static final String PARAMETERS = "Parameters";
// 匹配 Preparing: 后面的内容
private static final Pattern PREPARING_PATTERN =
Pattern.compile("Preparing:\\s*(.+?)(?:\\s*Parameters:|$)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
// 匹配 Parameters: 后面的内容
private static final Pattern PARAMETERS_PATTERN =
Pattern.compile("Parameters:\\s*(.+?)(?:\\s*Preparing:|$)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
public String genSql(String data) {
Pair<String, String> pair = splitSql(data);
List<String> params = parseParameters(pair.getValue());
return replacePlaceholders(pair.getKey(), params);
}
/**
* 按顺序替换 Preparing 中的 ? 占位符
*/
private static String replacePlaceholders(String preparing, List<String> paramList) {
StringBuilder sqlBuilder = new StringBuilder(preparing);
int placeholderIndex = 0; // 当前要替换的参数索引
for (int i = 0; i < sqlBuilder.length() && placeholderIndex < paramList.size(); i++) {
// 找到 ? 占位符
if (sqlBuilder.charAt(i) == '?') {
// 替换为格式化后的参数
sqlBuilder.replace(i, i + 1, paramList.get(placeholderIndex));
placeholderIndex++;
}
}
return sqlBuilder.toString();
}
private static List<String> parseParameters(String parameters) {
List<String> paramList = new ArrayList<>();
Matcher matcher = PARAM_PATTERN.matcher(parameters.trim());
while (matcher.find()) {
String value = matcher.group(1).trim(); // 参数值(如:18、'2025-01-01'、null)
String type = matcher.group(2).trim(); // 参数类型(如:Integer、Timestamp、String)
// 根据类型格式化参数
String formattedParam = formatParam(value, type);
paramList.add(formattedParam);
}
return paramList;
}
/**
* 根据参数类型格式化参数(核心:适配不同数据类型的 SQL 语法)
*/
private static String formatParam(String value, String type) {
// 1. 处理 NULL 值(无论类型,直接返回 NULL)
if (value == null || value.equalsIgnoreCase("null")) {
return "NULL";
}
// 2. 数值类型(整数、浮点数、布尔)
if (isNumericType(type)) {
// 布尔类型:MySQL 用 1/0,其他数据库可调整
if (type.equalsIgnoreCase("Boolean")) {
return value.equalsIgnoreCase("true") ? "1" : "0";
}
// 整数/浮点数:直接返回值(无需加引号)
return value;
}
// 3. 字符串/日期/枚举类型(需加单引号,处理单引号转义)
// 转义规则:将单个单引号 ' 替换为 ''(数据库标准转义)
String escapedValue = value.replace("'", "''");
return "'" + escapedValue + "'";
}
/**
* 判断是否为数值类型(无需加引号)
*/
private static boolean isNumericType(String type) {
return type.equalsIgnoreCase("Integer")
|| type.equalsIgnoreCase("Long")
|| type.equalsIgnoreCase("Double")
|| type.equalsIgnoreCase("Float")
|| type.equalsIgnoreCase("Short")
|| type.equalsIgnoreCase("Byte")
|| type.equalsIgnoreCase("Boolean")
|| type.equalsIgnoreCase("BigDecimal");
}
private Pair<String, String> splitSql(String data) {
String[] split = data.split("\\r?\\n|\\r");
if (split.length == 0) {
throw new RuntimeException("只支持一组Preparing和Parameters,使用换行区分");
}
String[] array = Arrays.stream(split).filter(StrUtil::isNotBlank).toArray(String[]::new);
if (array.length != 2) {
throw new RuntimeException("只支持一组Preparing和Parameters,使用换行区分");
}
if (!array[0].trim().contains(PREPARING)) {
throw new RuntimeException("第一行必须是Preparing开头");
}
if (!array[1].trim().contains(PARAMETERS)) {
throw new RuntimeException("第二行必须是Parameters开头");
}
String preparing = extractPreparing(array[0]);
String parameters = extractParameters(array[1]);
if (StrUtil.isBlank(preparing) || StrUtil.isBlank(parameters)) {
throw new RuntimeException("要生成的SQL数据格式错误");
}
return Pair.of(preparing, parameters);
}
/**
* 提取 Preparing: 后面的 SQL 语句
*/
public static String extractPreparing(String logText) {
Matcher matcher = PREPARING_PATTERN.matcher(logText);
if (matcher.find()) {
return matcher.group(1).trim();
}
return "";
}
/**
* 提取 Parameters: 后面的参数列表
*/
public static String extractParameters(String logText) {
Matcher matcher = PARAMETERS_PATTERN.matcher(logText);
if (matcher.find()) {
return matcher.group(1).trim();
}
return "";
}
}
@Data
public class LogDto {
private String data;
}
其他:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseResult<String> exceptionHandler(Exception e) {
log.info("internal.error: ", e);
return ResponseResult.fail(e.getMessage());
}
@ExceptionHandler(value = RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseResult<String> runTimeHandler(RuntimeException e) {
log.info("runTimeHandler.error: ", e);
return ResponseResult.fail(e.getMessage());
}
}
@Component
@ControllerAdvice
public class ResponseBodyWriteAdvice implements ResponseBodyAdvice<Object> {
private ObjectMapper objectMapper;
public ResponseBodyWriteAdvice(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof ResponseResult) {
return body;
} else if (body instanceof String) {
// 将 Content-Type 设为 application/json,返回类型是String时,默认 Content-Type = text/plain
HttpHeaders headers = response.getHeaders();
// 设置返回 json 格式时,处理中文乱码
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
try {
return objectMapper.writeValueAsString(ResponseResult.success(body));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return ResponseResult.success(body);
}
}
@Data
public class ResponseResult<T> implements Serializable {
public static final String SUC = "1";
public static final String FAIL = "0";
private String code;
private String msg;
private T data;
public ResponseResult() {
}
public ResponseResult(String code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
@JsonIgnore
public T getCheckedData() {
if (!this.code.equals(SUC)) {
throw new RuntimeException("调用异常");
}
return data;
}
public static <T> ResponseResult<T> success(T data) {
return new ResponseResult<>(SUC, null, data);
}
public static <T> ResponseResult<T> fail(String msg) {
return new ResponseResult<>(FAIL, msg, null);
}
public static <T> ResponseResult<T> fail(String code,String msg) {
return new ResponseResult<>(code, msg, null);
}
}
thymeleaf 模板
在 template 文件夹下创建 mybatis-sql-formatter.html, 页面由 deepseek 生成:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>MyBatis SQL 日志格式化工具</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #2b2b2b;
color: #ccc;
font-family: Consolas, "Microsoft YaHei", monospace;
padding: 20px;
}
.container {
width: 100%;
margin: 0 auto;
}
h2 {
color: #2e7df7;
margin-bottom: 20px;
border-bottom: 1px solid #444;
padding-bottom: 10px;
}
.input-area, .result-area {
margin-bottom: 20px;
}
textarea {
width: 100%;
height: 200px;
background: #333;
color: #ccc;
border: 1px solid #555;
border-radius: 4px;
padding: 10px;
font-size: 14px;
resize: vertical;
}
button {
background: #2e7df7;
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
}
button:hover {
background: #1a68e0;
}
.result-area h3 {
color: #fff;
margin-bottom: 10px;
}
.help {
margin-top: 30px;
padding: 15px;
background: #3a3a3a;
border-radius: 4px;
}
.help h3 {
color: #2e7df7;
margin-bottom: 10px;
}
.help p {
line-height: 1.6;
margin-bottom: 5px;
}
.error {
color: #ff4d4f;
margin: 10px 0;
font-size: 14px;
}
.loading {
color: #2e7df7;
margin: 10px 0;
display: none;
}
</style>
</head>
<body>
<div class="container">
<h2>MyBatis SQL 日志格式化工具</h2>
<!-- 日志输入区 -->
<div class="input-area">
<textarea id="logContent" placeholder="请粘贴 MyBatis 日志中的 Preparing 和 Parameters 相关行(示例如下):
[DEBUG] ==> Preparing: SELECT id FROM user WHERE age > ? AND name = ?
[DEBUG] ==> Parameters: 18(Integer), 张三(String)"></textarea>
<div class="loading" id="loading">正在格式化...(请稍候)</div>
<div class="error" id="errorMsg"></div>
<button onclick="formatSql()">格式化 SQL</button>
</div>
<!-- 结果展示区 -->
<div class="result-area" id="resultArea" style="display: none;">
<h3>格式化结果(可直接复制执行,目前只支持 mysql):</h3>
<textarea id="executableSql" readonly></textarea>
</div>
<!-- 使用帮助 -->
<div class="help">
<h3>使用帮助:</h3>
<p>1. 复制日志中包含 <code>Preparing:</code> 和 <code>Parameters:</code> 的两行内容(可包含日志前缀,如 [DEBUG]);</p>
<p>2. 粘贴到上方输入框,点击「格式化 SQL」按钮(无需刷新页面);</p>
<p>3. 下方将显示可直接在数据库执行的 SQL,支持字符串、数值、日期、NULL 等类型自动适配;</p>
<p>4. 支持特殊字符转义(如单引号 ' 自动转为 '')。</p>
</div>
</div>
<!-- 引入依赖:sql-formatter(使用unpkg稳定CDN,指定具体版本) -->
<!--<script src="https://unpkg.com/sql-formatter@12.0.5/dist/sql-formatter.min.js"></script>
<!– 引入 jQuery –>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>-->
<!-- 引入 jQuery(CDN 方式,无需本地下载) -->
<script src="/js/jquery.min.js"></script>
<script src="/js/sql-formatter.min.js"></script>
<script>
// AJAX 格式化 SQL
function formatSql() {
// 1. 获取输入值并清空状态
const logContent = $("#logContent").val().trim();
const $loading = $("#loading");
const $errorMsg = $("#errorMsg");
const $resultArea = $("#resultArea");
const $executableSql = $("#executableSql");
$errorMsg.text("");
$resultArea.hide();
$loading.show();
// 2. 校验输入
if (!logContent) {
$loading.hide();
$errorMsg.text("❌ 请输入日志内容!");
return;
}
// 3. 发送 AJAX POST 请求(JSON 格式传参)
$.ajax({
url: "/mybatis/sqllog/format", // 后端 AJAX 接口地址
type: "POST",
contentType: "application/json", // 告诉后端请求体是 JSON
data: JSON.stringify({ 'data': logContent }), // 构造 JSON 参数
dataType: "json", // 期望后端返回 JSON
success: function (response) {
if (response.code) {
// 成功:显示结果
// 3. 前端格式化 SQL(自定义配置)
if (response.data){
var rawSql = response.data;
const formattedSql = sqlFormatter.format(rawSql, {
language: 'mysql', // 选择的 SQL 方言
indent: ' ', // 缩进:2个空格(可改为 '\t' 制表符)
uppercase: true, // 关键字大写(如 SELECT、FROM)
linesBetweenQueries: 1, // 查询之间的空行数
denseOperators: false // 运算符前后留空格(如 a + b 而非 a+b)
});
$executableSql.val(formattedSql);
}else {
$executableSql.val(response.data);
}
$resultArea.show();
// 自动选中结果(方便复制)
$executableSql[0].select();
} else {
// 失败:显示错误信息
$errorMsg.text("❌ " + response.msg);
}
$loading.hide();
},
error: function (xhr, status, error) {
$loading.hide();
$errorMsg.text("❌ 网络错误:" + error + ",请刷新页面重试!");
}
});
}
// 快捷键支持:Ctrl+Enter 触发格式化
$("#logContent").keydown(function (e) {
if (e.ctrlKey && e.keyCode === 13) { // Ctrl+Enter
formatSql();
}
});
</script>
</body>
</html>
配置
spring.application.name=sqlformatter
spring.web.resources.static-locations=classpath:/static/
spring.thymeleaf.encoding=UTF-8
server.port=9001