架构设计之如何保护数据库性能与服务稳定性

92 阅读28分钟

架构设计之如何保护数据库性能与服务稳定性,即基于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中。

可能需要分步骤处理:

  1. 拦截Executor的query方法,获取参数对象。

  2. 检查参数对象中是否存在pageSize字段,可以通过反射或者参数类型来判断。

  3. 如果存在,检查其值是否超过1000,超过则抛出异常或修改为1000。

  4. 如果不存在pageSize参数,则修改SQL,添加limit 1000。

  5. 修改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等。这可能增加实现的复杂度。

所以,可能的解决思路是:

  1. 拦截StatementHandler的prepare方法,获取BoundSql中的SQL和参数。

  2. 分析参数对象,是否存在pageSize参数。

  3. 如果存在,检查其值是否超过1000,超过则调整。

  4. 如果不存在,则在SQL末尾添加LIMIT 1000。

  5. 修改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,超过则调整。

现在,考虑如何实现这一逻辑:

  1. 创建自定义拦截器,实现MyBatis的Interceptor接口,拦截StatementHandler的prepare方法,因为这里可以获取到最终的SQL语句和参数。

  2. 在拦截器中,获取BoundSql对象,分析其中的SQL语句。

  3. 检查参数对象中是否存在pageSize参数。可以通过反射查看参数对象是否有对应的字段或getter方法,或者参数是否为Map类型并包含pageSize键。

  4. 如果存在pageSize参数,则获取其值,如果超过1000,则抛出异常或修改为1000,并更新参数对象中的值。同时,确保SQL中的LIMIT子句使用该pageSize的值。例如,原SQL是否有LIMIT,如果有则替换,没有则添加。

  5. 如果不存在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等。

因此,可能的代码逻辑如下:

  1. 拦截StatementHandler的prepare方法,获取BoundSql对象。

  2. 获取参数对象:boundSql.getParameterObject()

  3. 使用MetaObject检查参数中是否存在pageSize属性。

  4. 如果存在:

a. 获取pageSize的值。

b. 如果超过1000,抛出异常或设置为1000。

c. 确保SQL中包含LIMIT子句,并参数化。

  1. 如果不存在:

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,所以不会有注入问题。

所以,可能的实现步骤:

  1. 拦截StatementHandler的prepare方法。

  2. 获取BoundSql对象。

  3. 获取原始SQL。

  4. 检查参数对象是否存在pageSize:

a. 使用MetaObject遍历参数对象的属性,或者检查Map中的键。

  1. 如果存在pageSize:

a. 获取值,如果超过1000,抛出异常或设置为1000。

b. 修改参数对象中的pageSize值。

c. 检查SQL中是否有LIMIT子句,如果没有,添加LIMIT #{pageSize}(假设参数化)。

d. 但如果原SQL已经有LIMIT,可能需要替换。

  1. 如果不存在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。

但如何实现这样的逻辑?

这里可能需要分步骤处理:

  1. 对于存在pageSize参数的情况:

a. 检查pageSize是否超过1000,超过则修改为1000。

b. 修改SQL,确保LIMIT子句的值是pageSize的值。

c. 如果原SQL没有LIMIT,则添加LIMIT #{pageSize}。

d. 如果原SQL有LIMIT,则替换其后的数值为#{pageSize}。

  1. 对于不存在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时,检查并限制其值。

这需要:

  1. 修改SQL,添加或替换LIMIT子句为LIMIT #{pageSize}。

  2. 在参数对象中设置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。

这可能更可靠,因为统一使用参数化的方式,并且处理参数对象。

因此,调整代码逻辑:

  1. 获取参数对象中的pageSize。

  2. 如果存在:

a. 检查是否超过1000,超过则设置为1000。

  1. 如果不存在:

a. 添加pageSize=1000到参数对象中。

  1. 修改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的拦截器。

总结,实现步骤包括:

  1. 创建拦截器,拦截StatementHandler的prepare方法。

  2. 解析参数对象,检查或添加pageSize参数。

  3. 修改SQL,添加或替换LIMIT子句。

  4. 使用反射更新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. 实现原理

  1. 拦截时机
    拦截StatementHandler.prepare()方法,此时SQL已解析但未执行。

  2. 参数处理

    • 检测参数对象中的pageSize
    • 存在且超过1000时自动修正为1000
    • 不存在时自动注入pageSize=1000
  3. SQL重写

    • 使用正则匹配现有LIMIT子句并替换为参数化形式
    • LIMIT时自动追加LIMIT #{pageSize}
  4. 兼容性处理

    • 支持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. 注意事项

  1. SQL注入防护
    强制使用#{}参数化形式,避免直接拼接值

  2. 复杂SQL兼容
    正则表达式可能无法处理嵌套子查询中的LIMIT,建议:

    • 使用SQL解析器(如JSqlParser)精准定位
    • 添加/* LIMIT */注释引导拦截器
  3. 事务一致性
    修改BoundSql的操作需保证线程安全

该方案在保证数据库查询安全的同时,兼顾了MyBatis的灵活性,可无缝集成到现有项目中