在企业级应用开发的复杂场景中,持久层作为数据交互的关键枢纽,其设计面临着数据查询灵活性与 SQL 可维护性的双重核心挑战。当业务逻辑日益复杂,传统方式下的 SQL 拼接往往变得繁琐且容易出错,开发者常常需要花费大量精力处理条件判断、空格添加、逗号去除等细节问题,不仅效率低下,还极大地影响了代码的可读性和可维护性。
MyBatis 作为 Java 生态中主流的持久层框架,其动态 SQL (code that is executed dynamically)功能犹如一把利刃,精准地切入了这一痛点。通过 XML 标签与 OGNL 表达式的巧妙结合,MyBatis 为复杂业务场景下的 SQL 拼接难题提供了优雅的解决方案。相较于 JDBC 等传统框架在 SQL 拼接时的种种不便 —— 开发者需时刻留意空格的添加、列表末尾逗号的处理等细节,稍有不慎便可能导致 SQL 语法错误 ——MyBatis 的动态 SQL 功能让开发者彻底摆脱了这些琐碎的困扰,将更多精力聚焦于业务逻辑的实现。
当然,掌握动态 SQL 并非轻而易举之事。然而,MyBatis 凭借其强大的动态 SQL 语言,显著提升了这一特性的易用性。对于熟悉 JSTL 或其他基于类 XML 语言文本处理器的开发者来说,MyBatis 的动态 SQL 元素并不会显得陌生,反而有一种似曾相识的亲切感。值得一提的是,在 MyBatis 3 版本中,基于 OGNL 的表达式发挥了重要作用,它替换了之前版本中大量的元素,使需要学习的元素种类大幅精简,较以往减少了一半以上,极大地降低了开发者的学习成本。
接下来,就让我们一同深看看MyBatis 动态 SQL 的标签是怎么使用的吧~
传统 JDBC 开发中,复杂条件查询依赖手动拼接 SQL,易引发三大痛点:
sql += "AND username = " + username
直接拼接用户输入WHERE AND
或SET ,
等格式错误MyBatis 通过标签化方案实现安全优雅的动态拼接,例如:
这样自动处理条件前缀 / 后缀,可以避免硬编码风险。
MyBatis 动态 SQL 的核心逻辑控制依赖于 OGNL(Object-Graph Navigation Language)表达式引擎,其强大的对象图导航能力为动态 SQL 的条件判断提供了底层支持。OGNL 的执行流程可拆解为三个核心技术环节:
Step1:表达式解析与 AST 构建
OGNL 引擎首先对test属性中的表达式进行词法分析和语法解析,生成抽象语法树(AST)。以典型条件username != null and age > 18为例:
MyBatis 通过OGNLExpressionParser类完成解析,最终生成Expression对象。该过程支持复杂表达式,包括:
Step2:上下文访问与参数解析
OGNL 表达式的求值依赖于表达式上下文(Expression Context),MyBatis 在此过程中做了两层参数封装:
1,单参数场景:
2,多参数场景:MyBatis 将参数封装为ParamMap对象(实现Map
比如一个方法selectUser(String username, Integer age)中,OGNL 可通过param1或username(若参数使用@Param注解)访问第一个参数。
对与集合数据,MyBatis 针对collection、list等集合参数做了特殊处理,在foreach标签中会自动将集合参数包装为Map,包含collection、list、array等键,确保 OGNL 表达式正确解析集合对象。
Step3:类型转换与参数绑定
OGNL 引擎在表达式求值后,MyBatis 会进行严格的类型转换以确保 SQL 参数安全,主要有两种方式。当Java 类型到 JDBC 类型映射时,MyBatis 会通过TypeHandlerRegistry查找对应的TypeHandler,例如Integer类型转换为 JDBC 类型 4(java.sql.Types.INTEGER),而String类型转换为 JDBC 类型 12(java.sql.Types.VARCHAR),也可以由我们支持自定义类型处理器,处理枚举、日期等特殊类型。
第二种是#{} 占位符安全处理,所有通过#{property}引用的参数,最终会生成?占位符,并通过PreparedStatement进行预编译,此时类型转换发生在数据库驱动层,避免 SQL 注入的同时保证类型安全。
对于特殊场景的处理方式:
在实际应用中,MyBatis 提供了完善的机制,以帮助我们在使用动态 SQL 时,达成性能优化与安全保障的平衡。
MyBatis 通过启用cacheEnabled="true"
开启二级缓存机制,缓存相同 SQL 的查询结果,并支持Ehcache
、Redis
等第三方缓存,减少数据库重复访问,但需根据业务场景设计缓存失效策略,防止脏读。批量执行器优化则通过defaultExecutorType="BATCH"
配置,适用于批量插入 / 更新场景,它将多条 SQL 缓存在内存中,使用addBatch()
和executeBatch()
一次性提交。
在 MySQL 8.0 和 InnoDB 引擎的测试环境下,批量插入 1000 条数据时,相比简单执行器,性能提升约 35%。此外,MyBatis 还能通过fetchType="lazy"
实现关联对象的延迟加载,避免加载过多数据,并利用resultMap
精确映射字段,减少不必要的列查询。
在安全编码规范方面,MyBatis 要求所有用户输入参数必须通过#{property}
占位符引用,利用预编译机制防止 SQL 注入,同时限制${}
仅用于动态字段名,并进行白名单校验。
例如,在排序字段使用时,需通过
校验。同时,应禁止通过动态 SQL 生成DROP TABLE
、TRUNCATE
等敏感操作语句。服务层也需对入参进行严格校验,如限制分页参数范围,并使用@Param
注解明确参数名称,避免 OGNL 解析歧义。此外,开启logImpl
(如LOG4J2
)记录执行的 SQL 语句,并对包含动态 SQL 的方法添加访问控制,限制敏感操作的调用频率。
MyBatis 动态 SQL 标签一共有如下几种:
标签分类 |
标签名称 |
核心功能 |
语法结构 |
核心属性 |
使用场景 |
示例代码 |
条件控制 |
|
单条件判断,根据表达式结果决定是否包含 SQL 片段 |
|
test:OGNL 表达式(如username != null) |
单条件过滤(如非空校验) |
xml |
条件控制 |
|
多条件分支选择(类似 Java 的 switch) |
|
无 |
互斥条件场景(如按 ID 或用户名查询) |
xml |
条件控制 |
|
|
|
test:OGNL 表达式 |
同上 |
同上 |
条件控制 |
|
|
|
无 |
同上 |
同上 |
结构优化 |
|
自动处理 WHERE 子句的冗余连接符(去除首个 AND/OR) |
|
无 |
复杂查询条件拼接(避免1=1硬编码) |
xml |
结构优化 |
|
自动处理 UPDATE 语句的冗余逗号,添加 SET 关键字 |
|
无 |
动态更新场景(如部分字段修改) |
xml |
结构优化 |
|
灵活去除 SQL 片段的前后缀字符(支持自定义前缀 / 后缀 / 覆盖规则) |
|
prefix/suffix:添加前后缀prefixOverrides/suffixOverrides:去除指定字符 |
复杂格式控制(如批量插入去逗号、自定义 WHERE/SET 逻辑) |
xml |
集合处理 |
|
遍历集合生成批量 SQL(如 IN 条件、批量插入) |
|
collection:集合对象item:元素别名separator:元素分隔符open/close:包裹符号 |
批量操作(如 IN 查询、批量插入 / 更新) |
xml |
代码复用 |
|
定义可复用的 SQL 片段 |
|
id:片段唯一标识 |
公共字段、条件或表名复用(减少重复代码) |
xml |
代码复用 |
|
引用已定义的 |
|
refid:目标片段 ID |
同上 |
xml |
高级控制 |
|
定义临时变量(MyBatis 3.2.4 + 新增) |
|
name:变量名value:OGNL 表达式 |
复杂表达式拆分(如预处理 LIKE 参数) |
xml |
下面我们逐一来看看:
基础条件判断
使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。比如:
这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title” 的参数值需要包含查找掩码或通配符字符)。
如果希望通过 “title” 和 “author” 两个参数进行可选搜索该怎么办呢?首先,我想先将语句名称修改成更名副其实的名称;接下来,只需要加入另一个条件即可。
注意事项:先判空再取值,避免NullPointerException
,如test="user != null and user.age > 18"
、
、
标签
有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了这三个标签组合起来用于实现类似 Java 中 switch - case
的多路分支选择逻辑。
MyBatis 会按照顺序依次判断
标签中的条件,当某个条件满足时,就会使用该
标签内的 SQL 片段,若所有
标签的条件都不满足,则使用
标签内的 SQL 片段。
动态 WHERE 生成
标签
如果我们在条件语句查询中直接使用where而不是标签,就会出现这种情况:
这时候如果没有匹配的条件最终这条 SQL 会变成这样:
SELECT * FROM BLOG
WHERE
这会导致查询失败。如果匹配的只是第二个条件,那么就会变成这样:
SELECT * FROM BLOG
WHERE
AND title like ‘my script’
这个查询也会失败。这个问题不能简单地用条件元素来解决。此时就可以使用WHERE
子句的编写,它只会在子元素返回任何内容的情况下才插入 “WHERE” ,若子句的开头为 “AND” 或 “OR”关键字,where 元素也会将它们去除。避免因条件为空而产生的 SQL 语法错误。
这样如果第一个
条件不满足,而第二个
条件满足,
标签会自动去掉 AND
关键字,避免产生语法错误。
安全更新
标签
主要用于 UPDATE
语句中,自动处理 SET
子句,会自动去除多余的逗号,避免 SQL 语法错误。适用于部分字段更新(如用户资料修改、商品库存调整)。
UPDATE sys_user
username = #{username},
email = #{email},
update_time = NOW()
WHERE id = #{id} AND version = #{version}
如果只有一个条件满足,
标签会自动去掉多余的逗号,无有效更新字段时不生成 SET 关键字。
灵活修剪
标签
trim标记是一个格式化的标记,它可以自定义前后缀过滤完成set或者是where标记的功能,比如我们这里通过trim代替where操作:
prefix:在当前位置添加一个前缀where
prefixoverride:去掉第一个and或者是or
prefix="("
添加前缀,suffix=")"
添加后缀,使用suffixOverrides=","实现
移除末尾逗号,相当于trim与prefix、suffix就能实现动态的标签或者元素符号添加。
foreach是用来对集合的遍历,这个和Java中的功能很类似。通常处理SQL中的in语句。
foreach 元素的功能非常强大,它允许我们指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符
我们可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
我们可以通过下面的方式实现批量查询:
也可以实现批量的数据插入:
INSERT INTO sys_user (username, email)
VALUES
(#{user.username}, #{user.email})
但是要注意在mapper中,集合参数需通过@Param("idList")
显式命名,避免Available parameters are [list]
错误。
在实际开发中我们会遇到许多相同的SQL,比如根据某个条件筛选,这个筛选很多地方都能用到,我们可以将其抽取出来成为一个公用的部分,这样修改也方便,一旦出现了错误,只需要改这一处便能处处生效了,此时就用到了
这个标签了。
当多种类型的查询语句的查询字段或者查询条件相同时,可以将其定义为常量,方便调用。
id, username, email, create_time, last_login_time
也可以通过查询来找到字段信息:
select * from student
而
比如我在比如你在com.yy.dao.firstMapper
这个Mapper的XML中定义了一个SQL片段如下:
id, username, email, create_time, last_login_time
当我子另一个com.yy.dao.secondMapper
查询中引用时,就可以这样实现:
也可以引用来实现字段查询:
但需要注意:refid这个属性就是指定
标签中的id值(唯一标识)。
在日常业务中除了单个数据的查询外,偶尔也会有部分需求可能需要我们采用多表关联查询的方式来进行,为此MyBatis也有对应的标签和集合标签一起来实现我们的动态SQL关联查询。
以常见的学生-老师这个关系为例,在学校中一个学生会有多个课程的老师,因此这样就形成了典型的一对多关系,那么我们可以通过下面这样的动态SQL实现查询一个学生关联了多少个老师的结果:
这里我们会发现一个新的标签这个元素
元素是 MyBatis
中最重要最强大的元素。它可以让我们从 90% 的 JDBC ResultSets
数据提取代码中解放出来,并在一些情形下允许我们进行一些 JDBC 不支持的操作。 我们通过一个简单例子来说明它的作用:
一般情况下我们在设计POJO或者Entity等实体类时,数据库中表的字段往往是与实体类(以User
类为例)的属性名称一致,我们就可以使用resultType
来返回。
当然,这是理想状态下,属性和字段名都完全一致的情况。但事实上,不一致的情况是有的,这时候我们的resultMap
就要登场了。如果User
类保持不变,但SQL
语句发生了变化,将id
改成了uid
。
那么,在结果集中,我们将会丢失id
数据。这时候我们就可以定义一个resultMap
,来映射不一样的字段。
然后,我们把上面的select
语句中的resultType
修改为resultMap="getUserByIdMap"
。这里面column
对应的是数据库的列名或别名;property
对应的是结果集的字段或属性。这就是resultMap
最简单,也最基础的用法:字段映射。
这样我们上面那个关联查询的结果集的意义就很清楚了:
标签
用于定义数据库查询结果到 Java 对象的映射关系。在一对多关联查询中,它可以将查询到的多表数据正确地映射到嵌套的 Java 对象结构中。
核心属性:
id
:为这个 resultMap
定义一个唯一的标识符,方便后续的 select
标签引用。type
:指定映射的 Java 对象类型,这里是 student1
类。子标签:
:用于映射数据库表的主键字段到 Java 对象的属性,确保数据的唯一性标识。例如,property="sid"
表示 Java 对象的 sid
属性,column="sid"
表示数据库表中的 sid
字段。
:用于映射普通字段到 Java 对象的属性。
:用于处理一对多的关联关系。
property
:指定 student1
类中用于存储关联对象集合的属性名,这里是 list
。ofType
:指定集合中元素的 Java 类型,这里是 teacher
类。这样就能通过一个mapper实现通过一个学生查询其对应的所有老师了。
import java.util.List;
public interface StudentMapper {
List findTeachersByStudentId(int studentId);
}
测试如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import java.util.List;
@SpringBootTest
public class StudentMapperTest {
@Autowired
private StudentMapper studentMapper;
@Test
public void testFindTeachersByStudentId() {
int studentId = 1;
List students = studentMapper.findTeachersByStudentId(studentId);
for (student1 student : students) {
System.out.println("Student ID: " + student.getSid());
System.out.println("Student Name: " + student.getSname());
for (teacher teacher : student.getList()) {
System.out.println(" Teacher ID: " + teacher.getTid());
System.out.println(" Teacher Name: " + teacher.getTname());
}
}
}
}
多对一关联查询在实际业务中也十分常见。例如,在一个教育系统里,多个学生可能会对应同一位老师;在一个电商系统中,多个订单可能会关联到同一个用户。下面以学生和班级的关系为例,多个学生属于同一个班级,这就是典型的多对一关联场景。这种场景下,我们常常需要查询学生信息的同时获取其所属班级的信息。
假设我们有两个表:student
表和 class
表。student
表结构:
字段名 | 类型 | 描述 |
---|---|---|
id | int | 学生 ID |
name | varchar | 学生姓名 |
class_id | int | 所属班级 ID |
class
表结构:
字段名 | 类型 | 描述 |
---|---|---|
id | int | 班级 ID |
class_name | varchar | 班级名称 |
然后我们确定一下实体类:
// Student.java
public class Student {
private int id;
private String name;
private Class clazz; // 注意这里使用 clazz 避免与 Java 关键字冲突
// 省略 getter 和 setter 方法
}
// Class.java
public class Class {
private int id;
private String class_name;
// 省略 getter 和 setter 方法
}
接着定义需要的mapper接口:
import java.util.List;
@Mapper
public interface StudentMapper {
List getAllStudents();
}
完成后我们通过xml来实现多对一查询:
这里我们要关注的核心点是
标签主要用于处理多对一的关联映射。在 MyBatis 的映射配置里,
标签能够把查询结果中的关联表数据映射到 Java 对象里的一个关联对象属性上。借助
标签,我们能够在查询学生信息时,同时把该学生所属班级的信息映射到学生对象的 class
属性中。
property="clazz"
:指定 Student
类中用于存储关联对象(班级)的属性名。javaType="com.example.entity.Class"
:指定关联对象的 Java 类型,也就是 Class
类。
和
:用于定义关联对象(班级)的主键和普通字段的映射关系。这样我们就实现查询所有学生信息,其中也会查询到每个学生关联的班级信息:
import com.example.entity.Student;
import com.example.mapper.StudentMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class StudentMapperTest {
@Autowired
private StudentMapper studentMapper;
@Test
public void testGetAllStudents() {
List students = studentMapper.getAllStudents();
for (Student student : students) {
System.out.println("Student ID: " + student.getId());
System.out.println("Student Name: " + student.getName());
if (student.getClazz() != null) {
System.out.println("Class ID: " + student.getClazz().getId());
System.out.println("Class Name: " + student.getClazz().getClass_name());
} else {
System.out.println("No associated class.");
}
System.out.println("----------------------");
}
}
}
除了这种多对一的查询方式外,
嵌套查询(Select 方式),除了通过 JOIN
语句在一个查询中获取关联数据,还可以使用嵌套查询的方式,即通过多个 SQL 查询来获取关联数据。这种方式适用于关联数据需要单独查询,或者关联数据比较复杂的场景。
select
属性:指定要调用的另一个 SQL 查询的 id
,这里是 com.example.mapper.ClassMapper.getClassById
。column
属性:指定传递给嵌套查询的参数,即 student
表中的 class_id
。 另外
还有一中特别少用的场景-自定义映射规则,
标签内可以使用更复杂的映射规则,例如使用
标签进行鉴别器映射,根据查询结果的某个字段值来决定使用不同的映射规则。不过这种用法相对较少,适用于关联对象有多种类型的情况。这里制作简单说明。
在一个教育系统中,一个学生可以选修多门课程,而一门课程也可以被多个学生选修。这就是典型的多对多场景,在这种场景下,我们通常需要查询某个主体关联的多个对象信息,或者通过某个对象查询与之关联的多个主体信息。
以学生和课程的多对多关系为例,需要三张表:student
表、course
表和中间表 student_course
。student
表结构:
字段名 | 类型 | 描述 |
---|---|---|
id | int | 学生 ID |
name | varchar | 学生姓名 |
course
表结构:
字段名 | 类型 | 描述 |
---|---|---|
id | int | 课程 ID |
course_name | varchar | 课程名称 |
student_course
表结构:
字段名 | 类型 | 描述 |
---|---|---|
student_id | int | 学生 ID,关联 student 表的 id |
course_id | int | 课程 ID,关联 course 表的 id |
然后定义实体,方式与上面差不多:
import java.util.List;
// Student.java
public class Student {
private int id;
private String name;
private List courses;
// Getters and Setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List getCourses() {
return courses;
}
public void setCourses(List courses) {
this.courses = courses;
}
}
// Course.java
public class Course {
private int id;
private String course_name;
// Getters and Setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getCourse_name() {
return course_name;
}
public void setCourse_name(String course_name) {
this.course_name = course_name;
}
}
完成Mapper 接口定义:
import java.util.List;
public interface StudentMapper {
List getAllStudentsWithCourses();
}
最后完成我们的多对多xml定义:
可以看到这是咱们还是用
property
:指定 Java 对象中用于存储关联对象集合的属性名。ofType
:指定集合中元素的 Java 类型。最后即可通过测试验证我们的xml实现结果了:
import com.example.entity.Student;
import com.example.mapper.StudentMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class StudentMapperTest {
@Autowired
private StudentMapper studentMapper;
@Test
public void testGetAllStudentsWithCourses() {
List students = studentMapper.getAllStudentsWithCourses();
for (Student student : students) {
System.out.println("Student ID: " + student.getId());
System.out.println("Student Name: " + student.getName());
System.out.println("Courses:");
if (student.getCourses() != null) {
for (Course course : student.getCourses()) {
System.out.println(" Course ID: " + course.getId());
System.out.println(" Course Name: " + course.getCourse_name());
}
} else {
System.out.println(" No courses selected.");
}
System.out.println("----------------------");
}
}
}
当然了这种情况下,
实现嵌套查询便于我们理解。比如在一个权限管理系统中,一个用户可能有多个权限和角色信息,那么我们可以通过嵌套查询来实现用户的权限角色多对多处理:
id,username,password,
real_name,email,phone,
status,create_time,update_time,
deleted
这种情况下除了要在userMapper中定义查询外,还需要在权限表和角色表定义mapper查询。
package com.yy.satokenapplication.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yy.satokenapplication.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* @author young
* @description 针对表【sys_user(系统用户表)】的数据库操作Mapper
* @createDate 2025-03-16 16:30:47
* @Entity com.yy.satokenapplication.entity.SysUser
*/
@Mapper
public interface SysUserMapper extends BaseMapper {
SysUser findUserWithRolesAndPermissionsById(@Param("userId") Long userId);
}
package com.yy.satokenapplication.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yy.satokenapplication.entity.SysPermission;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author young
* @description 针对表【sys_permission(系统权限表)】的数据库操作Mapper
* @createDate 2025-03-16 16:30:47
* @Entity com/yy/satokenapplication.entity.SysPermission
*/
@Mapper
public interface SysPermissionMapper extends BaseMapper {
List findPermissionsByUserId(@Param("userId")Long id);
}
package com.yy.satokenapplication.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yy.satokenapplication.entity.SysRole;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author young
* @description 针对表【sys_role(系统角色表)】的数据库操作Mapper
* @createDate 2025-03-16 16:30:47
* @Entity com.yy.satokenapplication.entity.SysRole
*/
@Mapper
public interface SysRoleMapper extends BaseMapper {
List findRolesByUserId(@Param("userId") Long userId);
}
最后让这个用户的xml进行多方关联从而在嵌套中实现多对多查询处理。
虽然动态sql使用起来让定制化业务很容易实现,但是在使用动态sql时,大家一定要注意一些编码规范,避免出现SQL注入的情况,以下有一些使用建议希望大家注意:
#{param}
代替${param}
,前者生成预编译语句,后者直接字符串替换 xml
ORDER BY ${sortField} ${sortOrder}
ORDER BY id ${sortOrder}
ORDER BY name ${sortOrder}
= ]]>
处理 XML 敏感符号,避免解析错误@NotNull(message="查询条件不能为空")
Java 层校验:
public List searchUsers(@Valid UserQuery query) {
// 业务逻辑校验,如分页参数合法性
if (query.getPageSize() > 100) {
throw new IllegalArgumentException("单次查询最大页数为100");
}
return userMapper.searchUsers(query);
}
XML 层防御: