借鉴mybatis源码 — 实现SQL动态拼接
2023-10-07 本文已影响0人
小胖学编程
在报表域的设计上,请求对象采用的是元数据格式的请求方式,在平台上只需要配置一些SQL模板。在解析引擎中实现SQL的拼接。那么如何实现SQL的动态拼接呢?
诉求:
- 需要支持#{}和${}占位符;
- 需要支持<if></if>动态标签;
- 需要支持SQL语法(例如in查询)
准备
需要引入QLExpress表达式:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>QLExpress</artifactId>
<version>3.3.2</version>
</dependency>
实现
下面两个方法是Mybatis的源码,其作用就是根据openToken
和closeToken
来找到占位的代码块,执行handler
中的逻辑来完成占位。
解析引擎(处理占位符)
这个方法是关键,用于定义占位符,同时实现多个handler实现类,完成对占位符的替换。
public class GenericTokenParser {
private final String openToken; //开始标记
private final String closeToken; //结束标记
private final TokenHandler handler; //标记处理器
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
/**
* 解析${}和#{}
*
* @param text 待解析的文本
* @return 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。
* 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现
*/
public String parse(String text) {
// 验证参数问题,如果是null,就返回空字符串。
if (text == null || text.isEmpty()) {
return "";
}
// 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。
int start = text.indexOf(openToken, 0);
if (start == -1) {
return text;
}
// 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,
// text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
// 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理
if (start > 0 && src[start - 1] == '\\') {
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
//重置expression变量,避免空指针或者老数据干扰。
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
//存在结束标记时
while (end > -1) {
//如果结束标记前面有转义字符时
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
//不存在转义字符,即需要作为参数进行处理
expression.append(src, offset, end - offset);
offset = end + closeToken.length();
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
handler处理器
处理${}占位符
public class ParameterMappingTokenHandler implements TokenHandler {
private Map<String, String> paramMap;
public ParameterMappingTokenHandler(Map<String, String> paramMap) {
this.paramMap = paramMap;
}
public String handleToken(String content) {
return paramMap.get(content);
}
}
处理#{}占位符
在#{}中,定义了字段的类型,便于处理in语法。
@Getter
public class SafeParameterMappingTokenHandler implements TokenHandler {
private Map<String, String> paramMap;
//分隔符
public static final String SPLIT = ";";
/**
* @param paramMap 参数映射对象
*/
public SafeParameterMappingTokenHandler(Map<String, String> paramMap) {
this.paramMap = paramMap;
}
@Override
public String handleToken(String content) {
//传入的格式,先判断是否存在要求的类型
if (StringUtils.isBlank(content)) {
return content;
}
//获取到数组类型
String[] param2Type = content.split(SPLIT);
ParameterType parameterType = getParameterType(param2Type);
String value = paramMap.get(param2Type[0]);
switch (parameterType) {
case STRING:
return formatString(value);
case LONG:
return formatLong(value);
case LIST_STRING:
return formatListString(value);
case LIST_LONG:
return formatListLong(value);
default:
throw BusinessErrorCode.PARAM_ERROR.of();
}
}
private String formatString(String value) {
value = StringUtils.replace(value, "'", "\\'");
value = StringUtils.replace(value, "\"", "\\\"");
value = StringUtils.replace(value, ";", "\\");
value = StringUtils.replace(value, "%", "\\%");
value = StringUtils.replace(value, "-", "\\-");
value = StringUtils.replace(value, "\\", "\\\\");
value = StringUtils.replace(value, "_", "\\_");
return String.format("%s%s%s", "'", value, "'");
}
private String formatLong(String value) {
return String.valueOf(Long.valueOf(value));
}
private String formatListString(String value) {
//获取到集合值
List<String> strings = BaseCollectionUtils.resolveList(value);
if (strings == null) {
return null;
}
return strings.stream()
.filter(StringUtils::isNotBlank)
.map(this::formatString)
.collect(Collectors.joining(","));
}
private String formatListLong(String value) {
//获取到集合值
List<String> strings = BaseCollectionUtils.resolveList(value);
if (strings == null) {
return null;
}
return strings.stream()
.filter(StringUtils::isNotBlank)
.map(this::formatLong)
.collect(Collectors.joining(","));
}
private ParameterType getParameterType(String[] param2Type) {
ParameterType parameterType;
if (param2Type.length == 1) {
parameterType = STRING;
} else {
parameterType = resolveParameterType(param2Type[1]);
}
return parameterType;
}
@Getter
public enum ParameterType {
STRING("string"), LIST_STRING("listString"), LONG("long"), LIST_LONG("listLong");
private String type;
ParameterType(String type) {
this.type = type;
}
public static ParameterType resolveParameterType(String type) {
for (ParameterType parameterType : values()) {
if (Objects.equals(parameterType.type, type)) {
return parameterType;
}
}
return STRING;
}
}
}
处理if标签
使用QLExpress表示式,简化了判null和判0的操作。
public class IfSpanTokenHandler implements TokenHandler {
private Map<String, String> paramMap;
public static final ExpressRunner RUNNER = new ExpressRunner();
static {
try {
RUNNER.addFunctionOfClassMethod("notEmpty", IfSpanTokenHandler.class.getName(), "isNotEmpty", new String[] {"String"}, null);
RUNNER.addFunctionOfClassMethod("notZero", IfSpanTokenHandler.class.getName(), "isNotZero", new String[] {"String"}, null);
} catch (Exception e) {
throw BusinessErrorCode.SYSTEM_ERROR.of(e);
}
}
public IfSpanTokenHandler(Map<String, String> paramMap) {
this.paramMap = paramMap;
}
@Override
public String handleToken(String content) {
//表达式
int startIndex = content.indexOf("'");
int endIndex = content.lastIndexOf("'");
String substring = content.substring(startIndex + 1, endIndex);
boolean res = true;
try {
DefaultContext<String, Object> context = new DefaultContext<>();
paramMap.keySet().forEach(k -> {
context.put(k, paramMap.get(k));
});
res = (boolean) RUNNER.execute(substring, context, null, false, false);
} catch (Exception e) {
throw BusinessErrorCode.REDIS_LOCK_ERROR.of(e);
}
if (res) {
return content.substring(content.indexOf(">") + 1);
} else {
return "";
}
}
public static boolean isNotEmpty(String value) {
return StringUtils.isNotBlank(value);
}
public static boolean isNotZero(String value) {
return StringUtils.isNotEmpty(value) && Long.parseLong(value) != 0;
}
}
工具类
public class PlaceholderUtil {
/**
* SQL解析器
*/
public static String parse(String sql, Map<String, String> paramMaps) {
String finalSql = sql;
GenericTokenParser ifSpanParser = new GenericTokenParser("<if", "</if>", new IfSpanTokenHandler(paramMaps));
finalSql = ifSpanParser.parse(finalSql);
GenericTokenParser parser = new GenericTokenParser("${", "}", new ParameterMappingTokenHandler(paramMaps));
finalSql = parser.parse(finalSql);
GenericTokenParser safeParser = new GenericTokenParser("#{", "}", new SafeParameterMappingTokenHandler(paramMaps));
return safeParser.parse(finalSql);
}
}
测试
测试类:
public static void main(String[] args) {
// ;listString定义了字段的类型是List<String>在解析的过程中,需要为每一个元素设置''
String sql = "select * from table where <if 'notEmpty(id)'>id in (#{id;listString}) and </if> name=#{name}";
System.out.println(parse(sql, Maps.builder().put("id", "123,456").put("name", "tim").build()));
}
测试结果:
select * from table where id in ('123','456') and name='tim'