在Java开发中,动态SQL是根据运行时条件拼装查询语句的常见需求。如果处理不当,既会出现SQL注入风险,也会让后续维护成本飙升。如何在两者间取得平衡,是后端工程师必须掌握的实践能力。

为什么动态SQL容易失衡
最直观的写法是使用字符串拼接,例如根据姓名和年龄筛选用户。这种方式可读性似乎很高,但一旦传入未校验的用户输入,就容易被注入。另一种极端是使用大量判断分支和隐式规则,导致代码像迷宫。
典型风险示例
// 危险示范:字符串拼接动态SQL
String sql = "SELECT * FROM user WHERE 1=1";
if (name != null) {
sql += " AND name = '" + name + "'"; // 可能被注入
}
if (age != null) {
sql += " AND age = " + age;
}
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);
上面的代码在name中传入 ' OR '1'='1 就会改变语义。使用 PreparedStatement 是最基本的安全底线。
使用参数绑定保证安全
PreparedStatement 通过占位符将参数与指令分离,从机制上避免注入。下面展示一个条件可选的动态查询写法:
String sql = "SELECT id,name,age FROM user WHERE 1=1";
List<Object> params = new ArrayList<>();
if (name != null) {
sql += " AND name = ?";
params.add(name);
}
if (age != null) {
sql += " AND age = ?";
params.add(age);
}
PreparedStatement ps = conn.prepareStatement(sql);
for (int i = 0; i < params.size(); i++) {
ps.setObject(i + 1, params.get(i));
}
ResultSet rs = ps.executeQuery();
这种做法安全且逻辑直接,但在字段很多时拼串仍显冗长,可维护性一般。
借助MyBatis提升可维护性
MyBatis 的动态标签能在XML或注解中清晰表达条件,同时底层依旧使用参数绑定。它让SQL结构直观,也便于DBA评审。
<select id="queryUser" parameterType="map" resultType="User">
SELECT id,name,age FROM user
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
这里 #{} 会被框架处理为占位符,不会直接拼接文本,安全由框架保障,SQL片段也易读。
白名单控制排序字段
ORDER BY 后面不能参数化,只能拼字段名。此时要用白名单限制,不能把前端字段直接放进SQL。
private static final Set<String> ALLOW_COLUMNS = Set.of("name", "age", "id");
public String buildOrder(String column, String dir) {
if (!ALLOW_COLUMNS.contains(column)) {
column = "id";
}
if (!"DESC".equalsIgnoreCase(dir)) {
dir = "ASC";
}
return " ORDER BY " + column + " " + dir;
}
分层让逻辑更清晰
把动态条件组装放在Repository或DAO层,业务层只传查询对象,不要散落拼SQL的代码。可以用查询对象封装规则:
public class UserQuery {
private String name;
private Integer age;
// getter setter 省略
public String toWhere() {
StringBuilder sb = new StringBuilder();
if (name != null) sb.append(" AND name = ?");
if (age != null) sb.append(" AND age = ?");
return sb.length() == 0 ? "" : " WHERE 1=1" + sb;
}
}
小结
平衡安全与可维护性,核心是用参数绑定堵住注入,用框架或分层减少杂乱拼串。对无法参数化的部分使用白名单,并保持SQL集中可视,就能在真实项目中稳妥落地。