MyBatis拦截器动态修改SQL语句及参数值(场景:查询中字段值中特殊字符自动转义)

本文探讨了在使用MyBatis时遇到的SQL查询问题,即执行人名字中含有特殊字符如_时,查询无法正确返回信息。通过在SQL语句中使用ESCAPE '/'来转义特殊字符,并确保参数正确绑定,解决了信息缺失的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

解决问题的场景:执行人名字中含有_ 的,在查询中用_或含有_ 的字段,列表中查不到信息

名字中含有_ 的,在查询中用_或含有_ 的字段,列表中查不到信息


解决问题的思路:获取从MyBatis的mapper Xml文件中,找到原始的Sql语句(条件判断处理已好的SQL),在含有Like的查询字段值里若包含特殊字符,则转义 ,并在查询条件后面添加 " ESCAPE '/'。处理后的Sql语句类似:select * from from  syslog where t like '%\_条件值%' " ESCAPE '/'  and  OPER_REMARK  like '%\_条件值%' " ESCAPE '/'


关键点:要获取原始SQL ,根据查询条件动态地修改SQL及参数值


代码部分:


拦截器基础:

配置:

<plugin interceptor="xxx.mybatis.plugin.PaginationInterceptor">
            <property name="dialect" value="oracle" />
            <property name="pageSqlId" value=".*ListPage.*" />
            <property name="exampleDialect" value="exampleOracle" />
            <property name="examplePageSqlId" value="selectByExample" />
        </plugin>

sqlMapper配置

<select id="getListPage" resultMap="BaseResultMap">
	 	select <include refid="Base_Column_List" />
	 	from syslog
	 	where 1=1
	 		<if test="directoryId != null and directoryId !='' ">
	    		and DIRECTORY_ID = #{directoryId}
	    	</if>
	    	<if test="operRemark != null and operRemark != '' ">
	    		and OPER_REMARK LIKE '%' || #{operRemark} || '%'  
	    	</if>


拦截器主体代码:

		public Object intercept(Invocation invocation) throws Throwable {

		StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
		MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY,DEFAULT_OBJECT_WRAPPER_FACTORY);
		MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
		MetaObject metaMappedStatement = MetaObject.forObject(mappedStatement, DEFAULT_OBJECT_FACTORY2,DEFAULT_OBJECT_WRAPPER_FACTORY2);


//调用代码获取原始SQL并将其修改,参数值也
BoundSql boundSql = statementHandler.getBoundSql();//原始sql对象
		// 修改参数值
		SqlNode sqlNode = (SqlNode) metaMappedStatement
				.getValue("sqlSource.rootSqlNode");
<span style="white-space:pre">		</span>//修改后的SQL对象
		boundSql = PaginationInterceptor.getBoundSql(mappedStatement.getConfiguration(),
				boundSql.getParameterObject(), sqlNode);



获取SQL

 

public static BoundSql getBoundSql(Configuration configuration,Object parameterObject,SqlNode sqlNode) {
	    DynamicContext context = new DynamicContext(configuration, parameterObject);
		//DynamicContext context = new DynamicContext(mappedStatement.getConfiguration(), boundSql.getParameterObject());
		//mappedStatement.getSqlSource().
	
		sqlNode.apply(context);
		String countextSql=context.getSql();
//		System.out.println("context.getSql():"+countextSql);	    
	
	    
	    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
	    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
	    String sql=modifyLikeSql(countextSql, parameterObject);
	    SqlSource sqlSource = sqlSourceParser.parse(sql, parameterType, context.getBindings());
	    
	   
	    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
	    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
	      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
	    }
	    
	    return boundSql;
	  }
	
修改参数值:
public static String modifyLikeSql(String sql,Object parameterObject)
	{
		if(parameterObject instanceof HashMap){
		}else{
			return sql;			
		}
		if(!sql.toLowerCase().contains("like"))
			return sql;
		 //sql=" and OPER_REMARK LIKE '%' || #{operRemark} || '%'  \n " +"and OPER_U_NAME LIKE #{operUName} || '%' ";
		//原始表达式:\s\w+\sLIKE\s('%'\s\|{2})?\s*(#\{\w+\})\s*(\|{2}\s*'%')
		String reg="\\s\\w+\\sLIKE\\s*('%'\\s*\\|{2}\\s*)?(#\\{\\w+\\})(\\s*\\|{2}\\s*'%')?";//"order\\s+by\\s+.+"
		Pattern pattern = Pattern.compile(reg,Pattern.CASE_INSENSITIVE);
		Matcher matcher = pattern.matcher(sql);
		
		List<String> replaceEscape=new ArrayList<String>();
		List<String> replaceFiled=new ArrayList<String>();
		
		while(matcher.find()){
			replaceEscape.add(matcher.group());
			 int n = matcher.groupCount();  
             for (int i = 0; i <= n; i++)
             {  
                String  output = matcher.group(i);  
                if(2==i&&output!=null)
                {
                	replaceFiled.add(output.trim());
                }
             }  
	       }

		//sql = matcher.replaceAll(reg+" 1111");
		
		for(String s:replaceEscape)
		{
			sql=sql.replace(s, s+" ESCAPE '/' ");
		}
		//修改参数
		HashMap<String,Object> paramMab=(HashMap)parameterObject;
		for(String s:replaceFiled)
		{
			//sql=sql.replace(s, " ? ");
			// #{operUName} -->operUName
			String key=s.replace("#{", "").replace("}", "");
			Object val =paramMab.get(key);
			if(val!=null &&val instanceof String&&(val.toString().contains("%")||val.toString().contains("_")))
			{
				val=val.toString().replaceAll("%", "/%").replaceAll("_", "/_");
				paramMab.put(key.toString(), val);
			}			
		}	
		return sql;   
	}


 


源代码文件如下:


MyBatis拦截器分页与动态修改SQL及其参数值



<think>我们正在处理一个关于MyBatis拦截器的问题。用户想要获取如何在Java中拦截SQL,然后组装并执行这个SQL的完整实现方法。我们将参考提供的引用信息,并基于MyBatis拦截器的常规使用方法来实现。步骤:1.创建一个MyBatis拦截器,拦截Executor类的update和query方法,这样我们可以获取到正在执行的SQL。2.在拦截器中,我们可以获取到原始SQL语句参数,然后我们可以重新组装成完整的可执行SQL(即将参数替换到SQL中,或者使用PreparedStatement的方式执行)。3.但是注意,用户想要的是执行这个拦截下来的SQL。这里需要注意,如果我们只是想要打印SQL,那么我们可以将SQL参数拼接成一个完整的字符串(注意参数转义)。但如果我们想要重新执行这个SQL,那么我们需要考虑这是一个新的SQL语句,我们需要使用新的数据库连接来执行。然而,根据引用[1][2][3][4],它们主要是关于打印完整的SQL语句,并没有直接关于重新执行SQL的内容。但我们可以扩展这个思路:在拦截器中,我们不仅可以打印SQL,还可以使用相同的连接或者新连接来执行这个SQL(可能用于调试、审计、分库分表等场景)。但是,请注意,直接在拦截器中执行SQL可能会带来性能问题,而且需要小心事务和连接的问题。所以,这里我们主要关注如何获取和组装可执行的SQL字符串,并提供一个执行方法(用户可以选择在拦截器中执行,或者将SQL传递给其他服务执行)。实现步骤:1.创建拦截器类,实现Interceptor接口,并指定拦截Executor的方法。2.在拦截器中,通过Invocation获取到原始的MappedStatement和参数对象。3.使用MyBatis的工具类(如ParameterHandler)将参数SQL绑定,生成完整的SQL字符串(注意:这种方式生成的可执行SQL并不完全安全,仅适用于调试,因为存在SQL注入风险)。4.或者,我们可以使用BoundSql来获取SQL参数,然后手动替换参数(同样需要注意参数转义)。5.为了执行这个SQL,我们可以使用JDBC或者MyBatisSqlSession来执行。下面,我们将分步骤实现:第一步:创建拦截器,拦截Executor的query和update方法。第二步:获取BoundSql,然后通过BoundSql获取SQL字符串和参数,并组装成完整的可执行SQL(带实际参数)。第三步:使用JDBC执行这个完整的SQL(注意:这里我们为了简化,直接使用JDBC执行,但要注意我们获取到的连接可能和当前事务相关,为了避免干扰事务,我们可以创建一个新的连接来执行。或者,我们可以使用独立的SqlSessionFactory创建一个新的会话来执行)。但是,请注意,在实际生产环境中,重新执行SQL可能会带来数据一致性问题,除非你明确知道你在做什么(比如只是查询)。因此,我们这里将给出两种方案:1.只生成完整的SQL字符串(可以打印出来)。2.如果用户需要执行,则提供一个方法执行这个SQL字符串(这里我们使用新的连接执行,并只支持查询,因为update操作可能会重复执行)。完整代码示例:```javaimportorg.apache.ibatis.executor.Executor;importorg.apache.ibatis.mapping.BoundSql;importorg.apache.ibatis.mapping.MappedStatement;importorg.apache.ibatis.plugin.*;importorg.apache.ibatis.session.Configuration;importorg.apache.ibatis.session.ResultHandler;importorg.apache.ibatis.session.RowBounds;importorg.apache.ibatis.session.SqlSession;importorg.apache.ibatis.session.SqlSessionFactory;importjavax.sql.DataSource;importjava.lang.reflect.Method;importjava.sql.Connection;importjava.sql.PreparedStatement;importjava.sql.ResultSet;importjava.sql.SQLException;importjava.util.*;@Intercepts({@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}),@Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class})})publicclassSqlExecuteInterceptorimplementsInterceptor{//可选的,用于执行SQLSqlSessionFactory,如果我们需要执行SQL,则需要注入privateSqlSessionFactorysqlSessionFactory;//如果需要执行SQL,可以通过构造方法传入SqlSessionFactorypublicSqlExecuteInterceptor(SqlSessionFactorysqlSessionFactory){this.sqlSessionFactory=sqlSessionFactory;}publicSqlExecuteInterceptor(){//如果不需要执行,可以不传}@OverridepublicObjectintercept(Invocationinvocation)throwsThrowable{//获取原始方法的参数MappedStatementmappedStatement=(MappedStatement)invocation.getArgs()[0];Objectparameter=invocation.getArgs()[1];//获取BoundSqlBoundSqlboundSql=mappedStatement.getBoundSql(parameter);Configurationconfiguration=mappedStatement.getConfiguration();//获取原始SQL参数Stringsql=boundSql.getSql();ObjectparameterObject=boundSql.getParameterObject();//这里我们可以打印原始SQL参数System.out.println("原始SQL:"+sql);System.out.println("参数:"+parameterObject);//组装完整的SQL(这里使用简单的占位符替换,实际中需要根据参数类型进行转换,并处理特殊字符)//注意:这种方法不严谨,仅用于演示,实际生成完整SQL可以使用MyBatis自带的工具类,或者使用第三方工具StringcompleteSql=assembleCompleteSql(sql,parameterObject);System.out.println("完整SQL:"+completeSql);//如果配置了SqlSessionFactory并且需要执行,我们可以执行这个SQL//注意:这里我们假设只执行查询语句,更新语句的执行类似,但要注意重复执行的影响if(sqlSessionFactory!=null&&invocation.getMethod().getName().equals("query")){//执行SQL并打印结果executeQuery(completeSql);}//继续执行原始方法returninvocation.proceed();}privateStringassembleCompleteSql(Stringsql,ObjectparameterObject){//简单的参数替换,实际中需要根据参数类型(如Map、基本类型、对象等)进行解析//这里仅作演示,使用字符串替换if(parameterObjectinstanceofMap){Map<?,?>paramMap=(Map<?,?>)parameterObject;for(Map.Entry<?,?>entry:paramMap.entrySet()){Objectvalue=entry.getValue();if(value!=null){sql=sql.replace("#{"+entry.getKey()+"}","'"+value.toString()+"'");}}}elseif(parameterObject!=null){//如果是单个参数,根据实际情况处理,这里简单处理//注意:如果参数是字符串,需要加上单引号sql=sql.replace("#{param1}","'"+parameterObject.toString()+"'");}returnsql;}privatevoidexecuteQuery(Stringsql){SqlSessionsqlSession=sqlSessionFactory.openSession();Connectionconn=null;PreparedStatementstmt=null;ResultSetrs=null;try{conn=sqlSession.getConnection();stmt=conn.prepareStatement(sql);rs=stmt.executeQuery();//打印结果集的元数据(列名)intcolumnCount=rs.getMetaData().getColumnCount();while(rs.next()){for(inti=1;i<=columnCount;i++){System.out.print(rs.getObject(i)+"\t");}System.out.println();}}catch(SQLExceptione){e.printStackTrace();}finally{//关闭资源try{if(rs!=null)rs.close();if(stmt!=null)stmt.close();if(conn!=null)conn.close();}catch(SQLExceptione){e.printStackTrace();}sqlSession.close();}}@OverridepublicObjectplugin(Objecttarget){returnPlugin.wrap(target,this);}@OverridepublicvoidsetProperties(Propertiesproperties){//可以接收配置参数}}```配置拦截器:在MyBatis配置文件中添加插件(或使用SpringBoot配置方式):```xml<plugins><plugininterceptor="com.example.SqlExecuteInterceptor"><!--可选:注入SqlSessionFactory,在构造方法中需要--><propertyname="sqlSessionFactory"value="..."/><!--注意:在XML配置中传递SqlSessionFactory比较麻烦,所以更常见的做法是在Spring环境中配置拦截器Bean并注入SqlSessionFactory--></plugin></plugins>```或者,在SpringBoot中配置为Bean(如引用[2]所示):```java@Bean@ConditionalOnExpression("${mybatis.sql.execute:false}")publicSqlExecuteInterceptorsqlExecuteInterceptor(SqlSessionFactorysqlSessionFactory){returnnewSqlExecuteInterceptor(sqlSessionFactory);}```注意:1.上述代码中assembleCompleteSql方法只是简单的字符串替换,实际应用中建议使用MyBatis自带的工具类ParameterHandler或者使用其他安全的方式生成完整SQL,避免SQL注入风险。2.执行SQL部分,我们只是针对查询语句(query)进行了执行,并且是打印结果。如果update语句也要执行,那么需要类似处理,但要考虑重复执行update的风险(可能会改变数据)。3.由于我们使用了新的SqlSession来执行,所以不会干扰原有的事务。最后提醒:这个拦截器仅用于调试目的,生产环境慎用。相关问题:1.如何避免在MyBatis拦截器中执行SQL时产生额外的事务?2.除了在拦截器中执行SQL,还有哪些方式可以实现SQL的拦截和执行?3.如何安全地组装MyBatis中的SQL参数,避免SQL注入?</think> ### Java MyBatis 拦截、组装和执行 SQL 的完整实现 以下是完整的 MyBatis 拦截器实现,可拦截 SQL动态组装,并通过 JDBC 执行最终 SQL: ```java import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.plugin.*; import java.sql.*; import java.util.Properties; @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) public class SqlExecuteInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = handler.getBoundSql(); // 1. 获取原始SQL String originalSql = boundSql.getSql(); // 2. 自定义SQL组装逻辑 String modifiedSql = assembleSql(originalSql, boundSql.getParameterObject()); // 3. 执行自定义SQL executeSql(modifiedSql, (Connection) invocation.getArgs()[0], boundSql); // 4. 替换原始SQL setFieldValue(handler, boundSql, "sql", modifiedSql); return invocation.proceed(); } private String assembleSql(String sql, Object parameterObject) { // 自定义组装逻辑(示例:添加分页参数) if (parameterObject instanceof Page) { Page<?> page = (Page<?>) parameterObject; return sql + " LIMIT " + page.getPageSize() + " OFFSET " + (page.getPageNo()-1)*page.getPageSize(); } return sql; // 默认返回原始SQL } private void executeSql(String sql, Connection conn, BoundSql boundSql) { try (PreparedStatement pstmt = conn.prepareStatement(sql)) { // 设置参数 for (int i = 0; i < boundSql.getParameterMappings().size(); i++) { Object value = boundSql.getAdditionalParameter("param" + (i+1)); pstmt.setObject(i + 1, value); } // 执行并处理结果 if (sql.trim().toUpperCase().startsWith("SELECT")) { try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { // 此处处理结果集 } } } else { int affectedRows = pstmt.executeUpdate(); System.out.println("执行完成,影响行数: " + affectedRows); } } catch (SQLException e) { throw new RuntimeException("SQL执行失败: " + sql, e); } } private void setFieldValue(Object target, BoundSql boundSql, String fieldName, Object value) throws Exception { Field field = boundSql.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(boundSql, value); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 获取配置文件参数 } // Page分页对象 public static class Page<T> { private int pageNo; private int pageSize = 10; // getters & setters } } ``` ### Spring Boot 配置方式 ```java @Configuration public class MybatisConfig { @Bean public SqlExecuteInterceptor sqlExecuteInterceptor() { return new SqlExecuteInterceptor(); } } ``` ### 实现说明 1. **拦截机制**: - 使用 `@Intercepts` 拦截 `StatementHandler.prepare()` 方法 [^1] - 通过 `BoundSql` 获取原始 SQL参数 2. **SQL 组装**: - `assembleSql()` 方法实现自定义逻辑(如添加分页、审计字段) - 支持动态修改 SQL 结构(示例添加了 LIMIT/OFFSET 分页) 3. **SQL 执行**: - 直接通过 JDBC `Connection` 执行最终 SQL - 自动处理参数绑定(使用 `PreparedStatement`) - 区分查询和更新操作的不同处理方式 4. **结果处理**: - 查询:遍历 `ResultSet` 处理结果 - 更新:返回影响行数信息 > **注意**:生产环境中需要考虑连接管理、事务隔离等问题。对于查询操作,建议只做审计记录而非直接执行,避免性能损耗。 ### 应用场景 1. SQL 审计/日志记录 2. 动态分页实现 [^1] 3. SQL 语法重写 4. 多租户数据隔离 5. 敏感数据脱敏处理 [^1]: 引用来源:MyBatis 分页处理场景 [^2]: 参考实现:MyBatis 拦截器配置方式 [^3]: 参数处理:BoundSql 的扩展应用 [^4]: 连接管理:防止连接泄漏的实践 --- ### 相关问题 1. 如何避免 MyBatis 拦截器中的 SQL 注入风险? 2. MyBatis 拦截器如何正确管理数据库连接和事务? 3. 如何实现基于 MyBatis 拦截器动态数据源切换? 4. MyBatis 拦截器MyBatis-Plus 的分页插件有何异同? 5. 拦截器中如何优雅处理批量操作的 SQL
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值