架构设计之如何保护数据库性能与服务稳定性,即基于MyBatis 3的InnerInterceptor实现数据查询条数的控制。具体要求是,当存在pageSize时不能超过1000,如果不存在则自动补全limit pageSize,以保护数据库查询。用户需要Java代码的实现。
首先,我需要回忆一下MyBatis的拦截器机制。MyBatis允许通过插件(Interceptor)来拦截四大核心对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)的方法。用户提到的InnerInterceptor可能是MyBatis Plus中的概念,但用户这里可能指的是MyBatis原生的拦截器接口。不过根据问题描述,应该使用MyBatis原生的拦截器来实现。
接下来,用户的需求分为两点:1. 检查pageSize是否存在,如果存在则不能超过1000;2. 如果不存在pageSize,则自动添加limit限制。这里的pageSize可能是指分页参数中的每页大小,通常与pageNum一起使用。但用户也可能指的是SQL中的limit参数,比如在查询时手动添加limit子句。
首先,需要确定拦截的时机。因为要修改SQL语句,所以应该在SQL执行前拦截StatementHandler的prepare方法,或者拦截Executor的query方法。但更常见的是拦截StatementHandler的prepare方法,因为此时可以修改BoundSql中的SQL语句。
然后,需要分析如何获取到请求中的pageSize参数。这里可能有两种情况:一种是分页参数通过RowBounds传递,另一种是参数中包含pageSize字段,或者直接在SQL中使用limit。比如,用户可能在自己的Mapper方法中使用了参数,例如有一个名为pageSize的参数,或者使用Map参数中包含pageSize的键。
如果用户使用的是MyBatis的分页插件,比如PageHelper,那么分页参数可能存储在ThreadLocal中,但这里用户可能希望自己实现一个拦截器来处理。
接下来,需要分析如何修改SQL语句。当pageSize存在时,检查其是否超过1000,超过则抛出异常或者自动设置为1000。如果不存在,则自动添加limit子句,比如默认限制为1000。但用户可能需要用户指定默认值,或者直接补全limit 1000。
但是,用户的具体需求是当不存在pageSize时自动补全limit pagesize。这里可能指的是如果原SQL中没有limit,则自动添加limit 1000;如果原SQL有limit,则检查其值是否超过1000,超过则进行调整。
不过,用户的问题描述可能更偏向于参数中的pageSize,而不是SQL中的limit。例如,在查询时,参数对象中包含pageSize字段,拦截器需要检查该字段的值,如果超过1000则报错,或者调整到1000;如果参数中没有pageSize字段,则自动添加limit 1000到SQL中。
可能需要分步骤处理:
-
拦截Executor的query方法,获取参数对象。
-
检查参数对象中是否存在pageSize字段,可以通过反射或者参数类型来判断。
-
如果存在,检查其值是否超过1000,超过则抛出异常或修改为1000。
-
如果不存在pageSize参数,则修改SQL,添加limit 1000。
-
修改BoundSql中的SQL语句和参数。
但是,修改SQL语句需要拦截StatementHandler的prepare方法,因为BoundSql是在那里生成的。因此,可能需要同时拦截StatementHandler的prepare方法,以修改SQL。
或者,可以只拦截Executor的query方法,检查参数,并在必要时修改参数中的pageSize值,同时确保生成的SQL包含正确的limit子句。但这种方法可能不够直接,因为SQL可能没有limit子句,而是通过参数动态添加的。
这里可能需要更深入的分析。比如,假设用户Mapper中的方法是这样写的:
@Select("SELECT * FROM table WHERE condition")
List queryData(@Param("pageSize") Integer pageSize);
然后在XML中可能使用动态SQL:
SELECT * FROM table LIMIT #{pageSize}这种情况下,拦截器可以检查参数中的pageSize是否超过1000,并调整它。但问题在于,如果用户没有在SQL中添加limit,那么即使参数中有pageSize,也不会生效。所以用户的需求可能是在没有pageSize参数的情况下,自动添加limit,比如默认限制为1000,而如果存在pageSize,则确保其不超过1000。
或者,用户希望无论原始SQL是否有limit,只要参数中有pageSize,就限制其值,否则自动添加limit 1000。
这可能需要动态修改SQL语句,无论原SQL是否有limit。例如:
原SQL:SELECT * FROM table
处理后:SELECT * FROM table LIMIT 1000
或者原SQL有limit,但参数中的pageSize超过1000,则修改limit后的值为1000。
但处理这种情况比较复杂,需要解析原SQL中的limit子句,替换或添加。对于不同的数据库,limit的语法可能不同,如MySQL是LIMIT,而Oracle使用ROWNUM等。这可能增加实现的复杂度。
所以,可能的解决思路是:
-
拦截StatementHandler的prepare方法,获取BoundSql中的SQL和参数。
-
分析参数对象,是否存在pageSize参数。
-
如果存在,检查其值是否超过1000,超过则调整。
-
如果不存在,则在SQL末尾添加LIMIT 1000。
-
修改BoundSql中的SQL和参数。
但需要注意,SQL可能已经包含LIMIT,这时候如何处理?比如,如果用户已经写了LIMIT,但参数中没有pageSize,这时候是否需要覆盖?或者保留原LIMIT,但检查其值?
根据用户的需求,可能需要不管原SQL是否有LIMIT,只要参数中没有pageSize,就自动添加LIMIT 1000。这可能会有问题,比如用户原本就有LIMIT子句,但参数中没有pageSize,这时候自动添加LIMIT可能导致语法错误。例如原SQL是SELECT * FROM table LIMIT 10,拦截器再添加LIMIT 1000,导致LIMIT 10 LIMIT 1000,语法错误。
因此,需要更智能的处理,比如检查原SQL是否已经有LIMIT子句。如果有,则不做处理;如果没有,则添加LIMIT 1000。或者,覆盖原来的LIMIT。但如何检测原SQL是否包含LIMIT?
这可能需要使用正则表达式来匹配,但不同数据库的语法不同,可能存在风险。例如,用户可能在注释中写了LIMIT,或者字符串中包含LIMIT关键字,导致误判。
因此,可能更安全的做法是,当参数中没有pageSize时,强制添加LIMIT 1000,覆盖原有的LIMIT。或者,用户可能希望不管原SQL是否有LIMIT,都应用这个规则。例如,即使原SQL有LIMIT 2000,当参数中没有pageSize时,自动改为LIMIT 1000。但这样可能修改了用户原本的意图,导致问题。
因此,用户的需求可能需要进一步明确。根据用户的问题描述,可能的情况是,无论原SQL是否有限制,当参数中没有pageSize时,自动添加LIMIT 1000,当参数中有pageSize时,检查其是否超过1000,超过则调整。
现在,考虑如何实现这一逻辑:
-
创建自定义拦截器,实现MyBatis的Interceptor接口,拦截StatementHandler的prepare方法,因为这里可以获取到最终的SQL语句和参数。
-
在拦截器中,获取BoundSql对象,分析其中的SQL语句。
-
检查参数对象中是否存在pageSize参数。可以通过反射查看参数对象是否有对应的字段或getter方法,或者参数是否为Map类型并包含pageSize键。
-
如果存在pageSize参数,则获取其值,如果超过1000,则抛出异常或修改为1000,并更新参数对象中的值。同时,确保SQL中的LIMIT子句使用该pageSize的值。例如,原SQL是否有LIMIT,如果有则替换,没有则添加。
-
如果不存在pageSize参数,则在SQL末尾添加LIMIT 1000,并可能修改参数对象,添加对应的参数。
但是,这里有一个问题:如果原SQL没有使用参数化的LIMIT,而是直接写死的数值,例如LIMIT 500,那么如何修改?此时,参数对象中没有pageSize参数,拦截器会添加LIMIT 1000,导致原SQL变成LIMIT 500 LIMIT 1000,语法错误。因此,这种情况需要处理原SQL中的LIMIT子句。
这似乎变得复杂,可能需要解析原SQL,替换其中的LIMIT子句。比如,无论原SQL是否有LIMIT,都替换为LIMIT 1000。或者,当参数中没有pageSize时,无论原SQL如何,都添加LIMIT 1000。这可能不符合用户预期,但根据问题描述,用户可能希望强制所有查询都有限制,除非明确指定了pageSize参数,并且其值不超过1000。
因此,可能的实现步骤:
-
拦截StatementHandler的prepare方法,获取BoundSql。
-
解析参数对象,检查是否存在pageSize参数。
-
如果存在,检查其值是否超过1000,超过则抛出异常或设置为1000。
-
如果不存在,修改SQL语句,添加LIMIT 1000。
-
处理过程中需要注意SQL的语法,例如是否有ORDER BY,是否已经有LIMIT等。
现在,如何实现这些步骤?
首先,如何检查参数中是否存在pageSize?参数可能是单个对象,也可能是Map或者多个参数(比如使用@Param注解的情况)。需要处理不同的参数类型。
例如,在MyBatis中,如果Mapper方法参数是多个,会被封装成Map,键为param1、param2或者注解指定的名称。如果参数是一个对象,则可以通过属性名访问。
因此,可能需要遍历参数,检查是否存在pageSize属性或键。
这可能需要使用MetaObject来处理参数对象,MyBatis提供了MetaObject工具类,可以方便地访问对象的属性。
其次,修改SQL语句,添加LIMIT子句。需要确保SQL的正确性,比如是否以分号结尾,是否已经包含LIMIT等。
因此,可能的代码逻辑如下:
-
拦截StatementHandler的prepare方法,获取BoundSql对象。
-
获取参数对象:boundSql.getParameterObject()
-
使用MetaObject检查参数中是否存在pageSize属性。
-
如果存在:
a. 获取pageSize的值。
b. 如果超过1000,抛出异常或设置为1000。
c. 确保SQL中包含LIMIT子句,并参数化。
- 如果不存在:
a. 检查原SQL是否包含LIMIT子句,如果没有,则添加LIMIT 1000。
b. 或者不管原SQL是否有LIMIT,都强制添加LIMIT 1000。
c. 如果添加了LIMIT,可能需要修改参数对象,添加对应的参数值。
但这里的问题是如何处理参数化查询。例如,原SQL可能已经包含LIMIT #{pageSize},此时参数中没有pageSize,添加LIMIT 1000可能需要修改SQL并添加对应的参数。
或者,当参数中没有pageSize时,直接修改SQL添加LIMIT 1000,而不使用参数化,即改为LIMIT 1000。但这样可能不够灵活,如果用户希望参数化,可能更合适。
或者,当添加LIMIT时,使用参数化,但此时需要修改参数对象,添加对应的参数值,比如pageSize=1000。这可能需要将参数对象转换为Map,并添加pageSize参数,或者如果是自定义对象,设置其pageSize属性。
但如何处理参数对象的类型?如果参数对象是不可变的,或者没有pageSize属性,这可能会很复杂。
因此,可能需要假设参数对象是Map类型,或者在添加pageSize时动态处理。
例如,当参数对象是Map时,可以添加一个pageSize键值对;如果不是Map,可能无法添加,这时候需要考虑如何处理。
因此,这种情况下,可能更适合在拦截器中,当发现参数对象中没有pageSize时,强制修改SQL添加LIMIT 1000,而不使用参数化,直接写成LIMIT 1000。这可能更简单,但存在SQL注入的风险,但这里因为值是固定的1000,所以不会有注入问题。
所以,可能的实现步骤:
-
拦截StatementHandler的prepare方法。
-
获取BoundSql对象。
-
获取原始SQL。
-
检查参数对象是否存在pageSize:
a. 使用MetaObject遍历参数对象的属性,或者检查Map中的键。
- 如果存在pageSize:
a. 获取值,如果超过1000,抛出异常或设置为1000。
b. 修改参数对象中的pageSize值。
c. 检查SQL中是否有LIMIT子句,如果没有,添加LIMIT #{pageSize}(假设参数化)。
d. 但如果原SQL已经有LIMIT,可能需要替换。
- 如果不存在pageSize:
a. 检查SQL是否有LIMIT子句,如果没有,添加LIMIT 1000。
b. 或者不管原SQL是否有LIMIT,都添加LIMIT 1000。
但这里的复杂性在于,如何正确修改SQL语句,并处理参数。
另一个思路是,不管原SQL是否有LIMIT,只要参数中没有pageSize,就添加LIMIT 1000。例如:
原SQL:SELECT * FROM table
修改后:SELECT * FROM table LIMIT 1000
如果原SQL已经有LIMIT,如SELECT * FROM table LIMIT 500,则修改为SELECT * FROM table LIMIT 1000。这可能覆盖用户原有的LIMIT,但根据用户的需求,可能希望强制最大为1000。
但这样可能不符合用户的实际需求,比如用户可能希望在某些情况下允许更大的pageSize,但根据问题描述,用户希望当存在pageSize时不能超过1000,所以拦截器的作用是强制限制。
因此,可能的实现是:
当参数中存在pageSize时,检查其值是否超过1000,超过则抛出异常或修改为1000,并确保SQL中的LIMIT使用该值;当参数中不存在pageSize时,在SQL中添加LIMIT 1000。
但如何确保SQL中的LIMIT使用该值?如果原SQL没有LIMIT子句,则需要添加;如果有,则替换。
这可能需要解析和修改SQL语句,找到LIMIT子句的位置,并替换其中的数值。或者,直接使用正则表达式替换。
例如,使用正则表达式匹配LIMIT后的数值,替换为1000,或者在末尾添加LIMIT 1000。
但正则表达式可能不够可靠,比如处理复杂的SQL语句时可能会有问题。
例如,处理以下情况:
SELECT * FROM table WHERE id > 100 LIMIT 50
需要将LIMIT 50改为LIMIT 1000,或者添加LIMIT 1000?
根据用户的需求,当存在pageSize参数时,如果参数中的pageSize为2000,会被截断为1000,此时SQL中的LIMIT应改为1000。如果参数中没有pageSize,则添加LIMIT 1000。
但如何将参数中的pageSize与SQL中的LIMIT关联起来?
可能更合理的做法是,无论原SQL是否有LIMIT,只要参数中有pageSize,就使用该参数的值,并确保不超过1000;如果没有参数,则添加LIMIT 1000。
但如何将参数中的pageSize与SQL中的LIMIT子句对应起来?可能需要动态生成SQL,将LIMIT子句替换为参数化的形式,或者直接拼接。
例如,原SQL是:
SELECT * FROM table
当参数中有pageSize=500,则SQL变为:
SELECT * FROM table LIMIT 500
当参数中没有pageSize,则SQL变为:
SELECT * FROM table LIMIT 1000
如果原SQL已经有LIMIT子句,比如:
SELECT * FROM table LIMIT 2000
当参数中没有pageSize,则改为LIMIT 1000。如果有pageSize=2000,则改为LIMIT 1000。
但如何实现这样的逻辑?
这里可能需要分步骤处理:
- 对于存在pageSize参数的情况:
a. 检查pageSize是否超过1000,超过则修改为1000。
b. 修改SQL,确保LIMIT子句的值是pageSize的值。
c. 如果原SQL没有LIMIT,则添加LIMIT #{pageSize}。
d. 如果原SQL有LIMIT,则替换其后的数值为#{pageSize}。
- 对于不存在pageSize参数的情况:
a. 修改SQL,添加LIMIT 1000。
但这里涉及动态修改SQL,并且处理参数化的问题。如果原SQL使用了参数化的LIMIT,比如LIMIT #{param},而参数中没有pageSize,此时可能需要添加LIMIT 1000,但如何处理参数?
这可能变得复杂。因此,或许更简单的做法是,当参数中没有pageSize时,直接在SQL末尾添加LIMIT 1000,而不使用参数化,这样就不需要处理参数对象。但这样可能不够灵活,不过对于用户的需求来说,可能已经足够。
现在,考虑编写拦截器的代码:
自定义拦截器需要实现Interceptor接口,并使用@Intercepts注解指定拦截的方法。例如,拦截StatementHandler的prepare方法:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PageSizeLimitInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
Object parameterObject = boundSql.getParameterObject();
String originalSql = boundSql.getSql();
// 检查是否存在pageSize参数
Integer pageSize = getPageSize(parameterObject);
String newSql = originalSql;
if (pageSize != null) {
if (pageSize > 1000) {
pageSize = 1000;
// 更新参数中的pageSize值
setPageSize(parameterObject, pageSize);
}
// 确保SQL中包含LIMIT #{pageSize}
newSql = updateOrAddLimit(originalSql, pageSize);
} else {
// 添加LIMIT 1000
newSql = addLimitIfAbsent(originalSql, 1000);
}
// 修改BoundSql中的SQL
if (!originalSql.equals(newSql)) {
ReflectUtil.setFieldValue(boundSql, "sql", newSql);
}
return invocation.proceed();
}
private Integer getPageSize(Object parameterObject) {
// 使用MetaObject检查parameterObject中是否存在pageSize属性
MetaObject metaObject = parameterObject == null ? null : MetaObject.forObject(parameterObject);
if (metaObject != null && metaObject.hasGetter("pageSize")) {
Object value = metaObject.getValue("pageSize");
if (value instanceof Integer) {
return (Integer) value;
}
}
// 如果参数是Map类型,检查是否有pageSize键
if (parameterObject instanceof Map) {
Map paramMap = (Map) parameterObject;
Object value = paramMap.get("pageSize");
if (value instanceof Integer) {
return (Integer) value;
}
// 检查param1、param2等(当使用@Param("pageSize")时)
for (int i = 1; i <= paramMap.size(); i++) {
String key = "param" + i;
if (paramMap.containsKey(key) && paramMap.get(key) instanceof Integer) {
// 这里假设参数顺序正确,可能不准确,需要更严谨的处理
return (Integer) paramMap.get(key);
}
}
}
return null;
}
private void setPageSize(Object parameterObject, Integer pageSize) {
// 设置parameterObject中的pageSize值
MetaObject metaObject = MetaObject.forObject(parameterObject);
if (metaObject.hasSetter("pageSize")) {
metaObject.setValue("pageSize", pageSize);
} else if (parameterObject instanceof Map) {
((Map<String, Object>) parameterObject).put("pageSize", pageSize);
}
// 其他情况可能无法设置,需要处理
}
private String updateOrAddLimit(String sql, Integer pageSize) {
// 使用正则表达式替换现有的LIMIT子句
Pattern pattern = Pattern.compile("\s+[Ll][Ii][Mm][Ii][Tt]\s+\d+", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(sql);
if (matcher.find()) {
// 替换现有的LIMIT数值
return matcher.replaceFirst(" LIMIT " + pageSize);
} else {
// 添加LIMIT子句
return sql + " LIMIT " + pageSize;
}
}
private String addLimitIfAbsent(String sql, int limit) {
// 检查是否已经有LIMIT
Pattern pattern = Pattern.compile("\s+[Ll][Ii][Mm][Ii][Tt]\s+\d+", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(sql);
if (!matcher.find()) {
return sql + " LIMIT " + limit;
}
return sql;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
注意,上述代码中的ReflectUtil.setFieldValue用于修改BoundSql的私有sql字段,需要使用反射,这可能在MyBatis的不同版本中存在差异,或者存在安全性问题。因此,需要确保反射操作是可行的。
此外,正则表达式处理LIMIT子句可能存在不足,比如无法处理复杂的SQL结构,或者LIMIT后面有参数占位符(如#{pageSize}),这会导致替换错误。
例如,如果原SQL是:
SELECT * FROM table LIMIT #{pageSize}
此时,正则表达式会匹配到LIMIT #{pageSize},并将其替换为LIMIT 1000,这会导致参数化失效,直接变成LIMIT 1000,而参数中的pageSize可能已经被修改为1000,但此时SQL中的#{pageSize}已经被替换为1000,导致参数无法传递。这会产生问题,因为原SQL的LIMIT已经是1000,而参数中的pageSize可能被修改,但实际此处已经被硬编码到SQL中。
这表明,上述方法可能不适用于参数化的LIMIT语句,而只能处理直接写数值的情况。因此,需要更精确的处理方式。
另一个方法是,无论原SQL是否参数化,都强制添加LIMIT #{pageSize},并修改参数对象中的pageSize值。例如,当原SQL没有LIMIT时,添加LIMIT #{pageSize},并将参数对象中的pageSize设置为1000(如果不存在)。
这可能需要更复杂的处理:
当参数中没有pageSize时,添加LIMIT #{pageSize},并将参数对象中的pageSize设为1000。
当参数中存在pageSize时,检查其值是否超过1000,若超过则设置为1000,并确保SQL中有LIMIT #{pageSize}。
这种情况下,不管原SQL是否有LIMIT,都会被替换或添加为LIMIT #{pageSize},并通过参数来控制值。
这需要处理参数对象,确保其包含pageSize参数,并且SQL中有对应的占位符。
例如,原SQL是:
SELECT * FROM table
处理后:
SELECT * FROM table LIMIT #{pageSize}
并且参数对象中添加pageSize=1000(如果原参数中没有)。
或者原SQL有LIMIT 2000,则替换为LIMIT #{pageSize},并设置参数中的pageSize为1000。
这种方式可能需要更复杂的SQL解析,但参数处理会更一致。
但如何修改SQL中的LIMIT子句为参数化?
这可能比较困难,因为不同的SQL写法不同。例如,原SQL可能使用LIMIT ?, ?,而参数是offset和pageSize。或者,原SQL使用LIMIT #{offset}, #{pageSize}。
因此,可能需要更智能的解析,或者限制拦截器只处理特定格式的LIMIT子句。
考虑到复杂度,可能更合理的做法是,假设用户在使用该拦截器时,其Mapper中的方法使用了参数化的LIMIT,或者允许拦截器强制添加LIMIT #{pageSize}并管理参数。
例如,当参数中没有pageSize时,添加LIMIT #{pageSize},并将pageSize设置为1000;当参数中存在pageSize时,检查并限制其值。
这需要:
-
修改SQL,添加或替换LIMIT子句为LIMIT #{pageSize}。
-
在参数对象中设置pageSize的值,确保不超过1000。
例如:
原SQL:SELECT * FROM table
修改后:SELECT * FROM table LIMIT #{pageSize}
参数对象中没有pageSize时,添加pageSize=1000。
原SQL:SELECT * FROM table LIMIT 2000
修改后:SELECT * FROM table LIMIT #{pageSize}
参数对象中的pageSize设置为1000。
原SQL:SELECT * FROM table LIMIT #{size}
参数对象中有size=2000,拦截器将size设置为1000。
但这里的问题是如何识别参数中的pageSize,可能用户使用了不同的参数名,如size、rows等。因此,可能需要让拦截器支持配置参数名,或者固定使用pageSize作为参数名。
根据用户的问题描述,参数名是pageSize,因此拦截器可以假设参数名为pageSize。
因此,代码逻辑可以调整为:
当参数中存在pageSize时,检查其值,否则添加pageSize=1000,并修改SQL添加LIMIT #{pageSize}。
这样,无论原SQL是否有LIMIT,都会被替换或添加为LIMIT #{pageSize},而参数中的pageSize被控制不超过1000。
这可能更可靠,因为统一使用参数化的方式,并且处理参数对象。
因此,调整代码逻辑:
-
获取参数对象中的pageSize。
-
如果存在:
a. 检查是否超过1000,超过则设置为1000。
- 如果不存在:
a. 添加pageSize=1000到参数对象中。
- 修改SQL,确保LIMIT子句为LIMIT #{pageSize}。
此时,无论原SQL是否有LIMIT,都会被替换为LIMIT #{pageSize},并且参数对象中的pageSize被控制。
这样,不管用户原来的SQL是LIMIT 2000还是其他,都会被拦截器统一处理。
但如何替换原SQL中的LIMIT子句为LIMIT #{pageSize}?
例如:
原SQL:SELECT * FROM table LIMIT 2000 → 替换为 SELECT * FROM table LIMIT #{pageSize}
原SQL:SELECT * FROM table → 添加 LIMIT #{pageSize}
这可以通过正则表达式匹配现有的LIMIT子句:
Pattern pattern = Pattern.compile("\s+[Ll][Ii][Mm][Ii][Tt]\s+.*?(\s+|$)", Pattern.CASE_INSENSITIVE);
String newSql = pattern.matcher(originalSql).replaceFirst(" LIMIT #{pageSize} ");
if (!newSql.equals(originalSql)) {
// 被替换
} else {
newSql = originalSql + " LIMIT #{pageSize}";
}
但需要注意,这样的正则表达式可能会错误地匹配到字符串中的LIMIT关键字,例如在注释或字符串中,导致错误的替换。因此,这种方法并不完全可靠,但在大多数情况下可能有效。
另外,替换后的SQL是LIMIT #{pageSize},这需要参数对象中存在pageSize参数,并且MyBatis能够处理它。因此,在拦截器中,必须确保参数对象中存在pageSize,并且其值被正确设置。
综上所述,代码的大致步骤为:
-
在拦截器中,获取参数对象。
-
检查是否存在pageSize参数,不存在则添加,并设为1000。
-
存在则检查并限制其值不超过1000。
-
修改SQL,确保LIMIT子句为LIMIT #{pageSize}。
这样,无论原SQL如何,都会强制使用参数化的pageSize,并且其值被限制在1000以内。
现在,编写代码实现:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PageSizeLimitInterceptor implements Interceptor {
private static final String PAGE_SIZE_PARAM = "pageSize";
private static final int MAX_PAGE_SIZE = 1000;
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
Object parameterObject = boundSql.getParameterObject();
String originalSql = boundSql.getSql();
MetaObject metaObject = parameterObject == null ?
MetaObject.forObject(new HashMap<>()) :
MetaObject.forObject(parameterObject);
// 获取或创建参数Map
Map<String, Object> paramMap = getParameterMap(parameterObject, metaObject);
// 处理pageSize参数
Integer pageSize = (Integer) paramMap.get(PAGE_SIZE_PARAM);
if (pageSize == null) {
pageSize = MAX_PAGE_SIZE;
paramMap.put(PAGE_SIZE_PARAM, pageSize);
} else if (pageSize > MAX_PAGE_SIZE) {
pageSize = MAX_PAGE_SIZE;
paramMap.put(PAGE_SIZE_PARAM, pageSize);
}
// 修改SQL
String newSql = updateOrAddLimit(originalSql);
// 更新BoundSql的sql和参数
if (!originalSql.equals(newSql)) {
ReflectUtil.setFieldValue(boundSql, "sql", newSql);
// 更新参数对象,可能需要处理不同的参数类型
if (!(parameterObject instanceof Map)) {
// 如果原参数不是Map,可能需要包装成Map,这里假设可以强制替换
// 注意:这可能不适用所有情况,根据实际情况调整
ReflectUtil.setFieldValue(boundSql, "parameterObject", paramMap);
}
}
return invocation.proceed();
}
private Map<String, Object> getParameterMap(Object parameterObject, MetaObject metaObject) {
Map<String, Object> paramMap;
if (parameterObject instanceof Map) {
paramMap = (Map<String, Object>) parameterObject;
} else {
paramMap = new HashMap<>();
// 将原参数对象的属性复制到Map中
String[] getterNames = metaObject.getGetterNames();
for (String name : getterNames) {
paramMap.put(name, metaObject.getValue(name));
}
}
return paramMap;
}
private String updateOrAddLimit(String sql) {
// 使用正则表达式替换或添加LIMIT #{pageSize}
Pattern pattern = Pattern.compile("\s+[Ll][Ii][Mm][Ii][Tt]\s+.*?(\s+|$)", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(sql);
if (matcher.find()) {
return matcher.replaceFirst(" LIMIT #{pageSize}");
} else {
return sql + " LIMIT #{pageSize}";
}
}
// 其他方法如plugin和setProperties...
}
注意,上述代码中的ReflectUtil需要能够修改BoundSql的sql字段和parameterObject字段。例如:
public class ReflectUtil {
public static void setFieldValue(Object target, String fieldName, Object value) {
try {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
但是,修改parameterObject可能会遇到问题,尤其是当原参数对象不是Map时,强制替换为Map可能导致Mapper方法期望的参数类型不匹配,从而出错。例如,如果Mapper方法参数是一个实体对象,而拦截器将parameterObject替换为Map,那么当MyBatis尝试从Map中获取参数时,可能无法正确映射到XML中的参数名。
因此,这种方法可能不适用于所有情况。更安全的做法是,当参数对象不是Map时,尝试动态添加pageSize属性到原参数对象中,但这需要参数对象有对应的setter方法。
例如,使用MetaObject的setValue方法:
if (metaObject.hasSetter(PAGE_SIZE_PARAM)) {
metaObject.setValue(PAGE_SIZE_PARAM, pageSize);
} else {
// 如果无法设置,可能需要抛出异常或记录日志
}
这仅当参数对象有pageSize的setter方法时才有效,否则无法添加。
因此,可能更可靠的方式是,只有当参数对象是Map或者包含pageSize属性时才进行处理,否则无法添加,此时可能无法自动添加LIMIT,或者抛出异常。
这增加了拦截器的复杂度,可能需要根据实际情况处理。
综上所述,虽然上述代码逻辑大致满足用户需求,但实际应用中可能需要根据具体情况进行调整,例如参数类型、SQL格式等。此外,反射修改BoundSql的sql字段和parameterObject可能在不同版本的MyBatis中有所不同,需要测试和验证。
最后,用户的问题中还提到“基于InnerInterceptor实现”,而InnerInterceptor是MyBatis Plus中的接口。如果用户正在使用MyBatis Plus,那么可能应该使用MyBatis Plus的拦截器接口来实现,这样会更方便。但用户的问题标题提到的是MyBatis 3.x,因此可能应该使用原生MyBatis的拦截器。
总结,实现步骤包括:
-
创建拦截器,拦截StatementHandler的prepare方法。
-
解析参数对象,检查或添加pageSize参数。
-
修改SQL,添加或替换LIMIT子句。
-
使用反射更新BoundSql的sql和参数。
需要注意处理参数对象类型、SQL解析的准确性以及反射修改私有字段的兼容性。
为了实现基于MyBatis 3.x的查询结果条数控制,可以通过自定义拦截器动态修改SQL和参数。以下是完整的代码实现:
1. 自定义拦截器实现
java
复制
import com.baomidou.mybatisplus.core.toolkit.PluginUtils; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory;
import java.sql.Connection; import java.util.*; import java.util.regex.Pattern;
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class PageSizeLimitInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(PageSizeLimitInterceptor.class);
private static final Pattern LIMIT_PATTERN = Pattern.compile(
"\s+[Ll][Ii][Mm][Ii][Tt]\s+.*?(\s+|$)",
Pattern.CASE_INSENSITIVE
);
private static final String PAGE_NUM_VALUE = "pageNum" ;
private static final String PAGE_SIZE_PARAM = "pageSize";
private static final int PAGE_SIZE_MAX_VALUE = 1000 ;
private static final int PAGE_NUM_INIT_VALUE = 1 ;
private List<String> excludeSqlList = null;
/**
* 拦截方法执行,对SQL语句和分页参数进行处理
*
* @param invocation 被拦截的调用对象,包含方法执行所需的信息
* @return 执行被拦截方法的返回结果
* @throws Throwable 如果方法执行过程中抛出异常
*/
@Override
public Object intercept(Invocation invocation) throws Throwable{
try {
// 获取被拦截的StatementHandler对象
StatementHandler handler = PluginUtils.realTarget(invocation.getTarget());
// 处理参数
MetaObject metaObject = handler == null ? SystemMetaObject.NULL_META_OBJECT :
SystemMetaObject.forObject(handler);
// 获取绑定的SQL信息
BoundSql boundSql = handler.getBoundSql();
// 获取SQL参数对象
Object parameterObject = boundSql.getParameterObject();
// 获取原始SQL语句
String sqlUpperCase = boundSql.getSql().toUpperCase();
boolean runProcessSqlStatus = checkProcessSql(handler,sqlUpperCase, excludeSqlList);
if (runProcessSqlStatus){
return invocation.proceed();
}
log.info("分页插件开始处理分页查询");
// 获取当前的分页大小
List<Long> pageNumSize = getPageNumSize(parameterObject);
if (isEmpty(pageNumSize) ){
return invocation.proceed();
}
long offset = (pageNumSize.get(0) - 1) * pageNumSize.get(1);
long pageSize = pageNumSize.get(1);
// 补全 SQL 语句
String paginatedSql = this.buildPaginationSql(boundSql.getSql(), offset, pageSize);
metaObject.setValue(DELEGATE_BOUND_SQL_VALUE, paginatedSql);
// 继续执行被拦截的方法,并返回其执行结果
return invocation.proceed();
}catch ( Exception e){
log.info("分页插件异常",e);
}
return invocation.proceed();
}
private boolean isNotEmpty(List<?> list ){
return list != null && !list.isEmpty();
}
private Integer getPageSize(MetaObject metaParam) {
try {
// 优先从Map类型参数获取
if (metaParam.getOriginalObject() instanceof Map) {
Map<?, ?> map = (Map<?, ?>) metaParam.getOriginalObject();
Object value = map.get(PAGE_SIZE_PARAM);
if (value instanceof Integer) return (Integer) value;
}
// 从对象属性获取
return (Integer) metaParam.getValue(PAGE_SIZE_PARAM);
} catch (Exception e) {
return null;
}
}
/**
* 获取分页参数
* 此方法旨在解析并返回分页查询所需的页码和页面大小
* 它支持从不同的参数类型中提取分页信息,例如直接传递的Page对象或包含分页信息的Map对象
*
* @param parameter 传递给SQL查询的参数对象,可以是Page类型或包含分页信息的Map类型
* @return 返回一个包含页码和页面大小的列表
*/
private List<Long> getPageNumSize(Object parameter ) {
// 初始化默认的页码和页面大小
long pageNum = 0;
long pageSize = 0;
// 如果参数是Page对象,直接获取分页信息
if (parameter instanceof Page) {
Page page = (Page) parameter;
pageNum = page.getCurrent();
pageSize = page.getSize();
if(page.searchCount()){
return new ArrayList<>();
}
}
// 如果参数是Map对象,尝试从中获取分页信息
if (parameter instanceof Map) {
Map<String, Object> paramMap = (Map<String, Object>) parameter;
Object pageObject = paramMap.getOrDefault(PAGE_VALUE,null);
// 检查Map中的分页对象是否为Page类型,并据此获取分页信息
if (pageObject instanceof Page && Objects.nonNull(pageObject)) {
Page page = (Page) pageObject;
pageNum = page.getCurrent();
pageSize = page.getSize();
if(page.searchCount()){
return new ArrayList<>();
}
} else {
// 如果Map中没有Page类型对象,则尝试获取单独的页码和页面大小值
pageNum = (Long) paramMap.getOrDefault(PAGE_NUM_VALUE, 0L);
pageSize = (Long) paramMap.getOrDefault(PAGE_SIZE_PARAM, 0L);
}
}
// 如果从参数中未获取到分页信息,则使用全局设置的默认值
pageNum = pageNum == 0 ? PAGE_NUM_INIT_VALUE : pageNum;
pageSize = pageSize == 0 ? PAGE_SIZE_MAX_VALUE : pageSize;
// 标记分页大小是否被修改
// 参数校验与设置
// 如果分页大小超过最大值,将其设置为最大值
if (pageSize > PAGE_SIZE_MAX_VALUE) {
pageSize = PAGE_SIZE_MAX_VALUE;
}
// 返回包含分页信息的列表,其中第一个元素是页码,第二个元素是页面大小
return Arrays.asList(pageNum,pageSize);
}
private void setPageSize(MetaObject metaParam, int value) {
try {
if (metaParam.getOriginalObject() instanceof Map) {
((Map<String, Object>) metaParam.getOriginalObject()).put(PAGE_SIZE_PARAM, value);
} else if (metaParam.hasSetter(PAGE_SIZE_PARAM)) {
metaParam.setValue(PAGE_SIZE_PARAM, value);
}
} catch (Exception e) {
// 参数对象不支持pageSize时忽略
}
}
private boolean checkProcessSql(StatementHandler handler ,String upperCaseSql,List<String> keyWordList){
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(handler);
MappedStatement ms = mpSh.mappedStatement();
SqlCommandType sct = ms.getSqlCommandType();
if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
return true;
}
if ( LIMIT_PATTERN.matcher(upperCaseSql).find()){
return true;
}
if (upperCaseSql.contains(LIMIT_VALUE) || upperCaseSql.contains(T_LIMIT_VALUE)){
return true;
}
if (upperCaseSql.contains(SqlMethod.SELECT_COUNT.getSql())){
return true;
}
String upperCase = upperCaseSql.replace(" ", "").replace("\n", "").replace("\t", "");
// 检查是否需要补全 SQL,如果非 SELECT 或 SELECTCOUNT查询开头的,直接返回
if (!upperCase.startsWith(SELECT_VALUE) || upperCase.startsWith(SELECT_VALUE_COUNT)) {
return true;
}// 检查是否需要补全 SQL,如果非 keyWordList 不为空且 sql中包括关键词的,直接返回
if (isNotEmpty(keyWordList) && keyWordList.stream().anyMatch(upperCaseSql::contains)) {
return true;
}
return false;
}
/**
* 这段Java代码的功能是构建带有分页功能的SQL查询语句。具体步骤如下:
*
* 1. 检查原始SQL是否已经包含 `LIMIT` 关键字,如果是,则直接返回原始SQL。
* 2. 如果不包含 `LIMIT`,则在原始SQL后面追加 `LIMIT` 关键字。
* 3. 根据 `offset` 和 `limit` 的值,生成最终的SQL语句:
* - 如果 `offset` 为0,则只追加 `limit`。
* - 否则,追加 `offset` 和 `limit`,中间用逗号分隔。
*/
public String buildPaginationSql(String originalSql, long offset, long limit) {
if (originalSql.toUpperCase().contains(LIMIT_VALUE) || originalSql.toUpperCase().contains(T_LIMIT_VALUE)){
return originalSql;
}
StringBuilder sql = new StringBuilder(originalSql).append(LIMIT_VALUE);
if (offset == 0L) {
sql.append(limit);
} else {
sql.append(offset);
sql.append(StringPool.COMMA).append(limit);
}
return sql.toString();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
2. 配置说明
注册拦截器
在MyBatis配置文件中添加拦截器:
xml
复制
<configuration>
<plugins>
<plugin interceptor="com.yourpackage.PageSizeLimitInterceptor"/>
</plugins>
</configuration>
运行 HTML
Mapper接口示例
java
复制
public interface UserMapper {
// 自动应用LIMIT规则
List<User> findUsers(@Param("pageSize") Integer pageSize);
// 无pageSize参数时自动添加LIMIT 1000
List<User> findAllUsers();
}
XML映射文件
xml
复制
<select id="findUsers" resultType="User">
SELECT * FROM users WHERE status = 1
<if test="pageSize != null">
LIMIT #{pageSize}
</if>
</select>
<select id="findAllUsers" resultType="User">
SELECT * FROM users
</select>
运行 HTML
3. 实现原理
-
拦截时机
拦截StatementHandler.prepare()方法,此时SQL已解析但未执行。 -
参数处理
- 检测参数对象中的
pageSize值 - 存在且超过1000时自动修正为1000
- 不存在时自动注入
pageSize=1000
- 检测参数对象中的
-
SQL重写
- 使用正则匹配现有
LIMIT子句并替换为参数化形式 - 无
LIMIT时自动追加LIMIT #{pageSize}
- 使用正则匹配现有
-
兼容性处理
- 支持Map类型和POJO类型参数
- 通过MetaObject智能操作参数对象
4. 使用效果
| 调用方式 | 实际执行SQL | 参数值 |
|---|---|---|
mapper.findUsers(2000) | SELECT * FROM users LIMIT ? | 1000 |
mapper.findUsers(500) | SELECT * FROM users LIMIT ? | 500 |
mapper.findAllUsers() | SELECT * FROM users LIMIT ? | 1000 |
5. 高级配置
通过@InterceptorProperty支持自定义参数名和最大值:
java
复制
@Intercepts(...)
public class PageSizeLimitInterceptor implements Interceptor {
private String pageSizeParam = "pageSize";
private int maxPageSize = 1000;
@Override
public void setProperties(Properties properties) {
this.pageSizeParam = properties.getProperty("paramName", "pageSize");
this.maxPageSize = Integer.parseInt(
properties.getProperty("maxSize", "1000")
);
}
// 其余代码使用pageSizeParam和maxPageSize变量
}
配置时指定参数:
xml
复制
<plugin interceptor="com.yourpackage.PageSizeLimitInterceptor">
<property name="paramName" value="maxRows"/>
<property name="maxSize" value="500"/>
</plugin>
运行 HTML
6. 注意事项
-
SQL注入防护
强制使用#{}参数化形式,避免直接拼接值 -
复杂SQL兼容
正则表达式可能无法处理嵌套子查询中的LIMIT,建议:- 使用SQL解析器(如JSqlParser)精准定位
- 添加
/* LIMIT */注释引导拦截器
-
事务一致性
修改BoundSql的操作需保证线程安全
该方案在保证数据库查询安全的同时,兼顾了MyBatis的灵活性,可无缝集成到现有项目中