1. 前言
在项目中,当使用 LambdaUpdateWrapper 执行 update 操作时,发现字段无法像实体保存那样自动加密,必须手动赋值加密后的值。于是深入分析了加密拦截器的实现逻辑。
2. 拦截器实现
2.1 拦截器实现
拦截器关注update
操作,代码大致如下:
private final EncryptUtil encryptUtil;
// SoftReference缓存加密字段
private static final Map<Class<?>, SoftReference<List<Field>>> ENCRYPT_FIELD_CACHE =
new ConcurrentHashMap<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
Set<Object> processed = Collections.newSetFromMap(new IdentityHashMap<>());
for (Object arg : args) {
if (arg instanceof MappedStatement) {
continue;
}
if (arg instanceof Map<?, ?> map) {
if (map.isEmpty()) {
return arg;
}
for (Map.Entry<?, ?> entry : map.entrySet()) {
var key = entry.getKey();
var value = entry.getValue();
if (key.toString().startsWith("param") || key.toString().startsWith("et")
|| key.toString().startsWith("collection")) {
if (value instanceof ArrayList<?> list) {
for (Object item : list) {
encryptField(item, processed);
}
} else {
encryptField(value, processed);
}
}
}
} else {
encryptField(arg, processed);
}
}
return invocation.proceed();
}
/**
* 加密对象的所有加密字段,避免嵌套加密
*/
private void encryptField(Object object, Set<Object> processed) throws IllegalAccessException {
if (object == null || processed.contains(object)) {
return;
}
processed.add(object);
List<Field> encryptFields = getEncryptFields(object.getClass());
for (Field field : encryptFields) {
Object value = field.get(object);
if (value != null) {
String encrypted = encryptUtil.mpEncrypt(value.toString());
field.set(object, encrypted);
log.debug("字段 [{}] 已加密,原值: [{}], 加密后: [{}]", field.getName(), value,
encrypted);
}
}
}
/**
* 获取并缓存带有@Encrypt注解的字段
*/
private List<Field> getEncryptFields(Class<?> clazz) {
SoftReference<List<Field>> ref = ENCRYPT_FIELD_CACHE.get(clazz);
List<Field> fields = ref != null ? ref.get() : null;
if (fields == null) {
fields = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Encrypt.class)) {
field.setAccessible(true);
fields.add(field);
}
}
ENCRYPT_FIELD_CACHE.put(clazz, new SoftReference<>(fields));
log.debug("缓存类 [{}] 的加密字段: {}", clazz.getName(),
fields.stream().map(Field::getName).toList());
}
return fields;
}
在当前代码的实现中,intercept
函数接收到arg
参数LambdaUpdateWrapper
类型时,无法通过反射机制正常获取字段值(具体详见下文),进而无法执行加密操作。
2.2 LambdaUpdateWrapper
加密实现
private void encryptLambdaUpdateWrapper(LambdaUpdateWrapper<?> wrapper, EncryptUtil encryptUtil)
throws IllegalAccessException {
Object entity = wrapper.getEntity();
List<Field> encryptFields = null;
if (entity != null) {
encryptFields = getEncryptFields(entity.getClass());
for (Field field : encryptFields) {
Object value = field.get(entity);
if (value != null) {
String encrypted = encryptUtil.mpEncrypt(value.toString());
field.set(entity, encrypted);
log.debug("LambdaUpdateWrapper实体字段 [{}] 已加密,原值: [{}], 加密后: [{}]",
field.getName(), value, encrypted);
}
}
}
// 处理set方法设置的字段
List<String> sqlSet = (List<String>) ReflectUtil.getFieldValue(wrapper, "sqlSet");
if (sqlSet != null && entity != null) {
// 构建加密字段名映射,避免多次遍历
Map<String, Field> encryptFieldMap = new java.util.HashMap<>();
for (Field field : encryptFields) {
encryptFieldMap.put(field.getName(), field);
}
for (int i = 0; i < sqlSet.size(); i++) {
String setExpr = sqlSet.get(i); // 形如 name='xxx'
String[] parts = setExpr.split("=", 2);
if (parts.length == 2) {
String fieldName = parts[0].trim();
Field field = encryptFieldMap.get(fieldName);
if (field != null) {
String value = parts[1].replaceAll("'", "").trim();
String encrypted = encryptUtil.mpEncrypt(value);
sqlSet.set(i, fieldName + "='" + encrypted + "'");
log.debug("LambdaUpdateWrapper setSql字段 [{}] 已加密,原值: [{}], 加密后: [{}]",
fieldName, value, encrypted);
}
}
}
}
}
针对LambdaUpdateWrapper参数的加密处理逻辑,主要包含以下两个关键部分:
实体对象处理:由于LambdaUpdateWrapper对实体对象进行了封装,需要先提取出其中封装的原始entity对象。通过反射机制获取实体类中标记了加密注解的字段,并对这些字段的值进行加密处理。这一步骤确保了通过set(entity)方式设置的字段能够被正确加密。
SQL片段处理:LambdaUpdateWrapper内部维护了一个sqlSet属性(List<String>类型),它会将实体字段引用转换为数据库列名,并与对应的值拼接成SQL片段(如"username='admin'")。处理时需要解析这些SQL片段,通过字段名反向查找实体类中的对应属性,判断是否包含加密注解,进而决定是否需要对值部分进行加密处理。这一步骤确保了通过setSql()等直接拼接SQL的方式设置的字段值也能被加密。
3. 放弃
虽然针对LambdaUpdateWrapper设计了完整的加密处理方案,但经过深入评估后决定放弃实现,主要基于以下考虑:
字段映射关系缺失:由于MyBatis-Plus的字段名转换机制(如驼峰命名userName转换为下划线user_name,开发人员创建数据表字段别名等),无法准确建立数据库列名与实体属性名的双向映射关系,导致无法可靠识别需要加密的字段。
性能损耗问题:方案涉及大量SQL字符串的解析、分割和重组操作,这种基于字符串处理的方式会带来显著的性能开销,特别是在高频更新场景下可能成为系统瓶颈。
维护成本较高:该方案需要深度依赖LambdaUpdateWrapper的内部实现细节(如sqlSet字段结构),这些未公开的实现可能在后续版本变更时导致兼容性问题,增加长期维护成本。