[重大更新] 重写翻译和脱敏实现 使用jackson tree解析加ResponseBodyAdvice处理数据的方案 实现可批量翻译大幅度提高效率 用法与灵活性不变

This commit is contained in:
疯狂的狮子Li
2026-03-30 14:23:35 +08:00
parent a1f8df90cf
commit 8300f65640
24 changed files with 768 additions and 254 deletions

View File

@@ -1,10 +1,10 @@
package org.dromara.common.translation.annotation;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import org.dromara.common.translation.core.handler.TranslationHandler;
import tools.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.*;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 通用翻译注解
@@ -13,8 +13,7 @@ import java.lang.annotation.*;
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@JacksonAnnotationsInside
@JsonSerialize(using = TranslationHandler.class)
@Documented
public @interface Translation {
/**

View File

@@ -1,54 +1,23 @@
package org.dromara.common.translation.config;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.translation.annotation.TranslationType;
import org.dromara.common.translation.core.TranslationInterface;
import org.dromara.common.translation.core.handler.TranslationBeanSerializerModifier;
import org.dromara.common.translation.core.handler.TranslationHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.dromara.common.translation.core.handler.TranslationJsonFieldProcessor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import tools.jackson.databind.ser.SerializerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 翻译模块配置类
*
* @author Lion Li
*/
@Slf4j
@AutoConfiguration
public class TranslationConfig {
@Autowired
private List<TranslationInterface<?>> list;
@PostConstruct
public void init() {
Map<String, TranslationInterface<?>> map = new HashMap<>(list.size());
for (TranslationInterface<?> trans : list) {
if (trans.getClass().isAnnotationPresent(TranslationType.class)) {
TranslationType annotation = trans.getClass().getAnnotation(TranslationType.class);
map.put(annotation.type(), trans);
} else {
log.warn(trans.getClass().getName() + " 翻译实现类未标注 TranslationType 注解!");
}
}
TranslationHandler.TRANSLATION_MAPPER.putAll(map);
}
@Bean
public JsonMapperBuilderCustomizer translationInitCustomizer() {
return builder -> {
SerializerFactory serializerFactory = builder.serializerFactory();
serializerFactory = serializerFactory.withSerializerModifier(new TranslationBeanSerializerModifier());
builder.serializerFactory(serializerFactory);
};
public TranslationJsonFieldProcessor translationJsonFieldProcessor(List<TranslationInterface<?>> list) {
return new TranslationJsonFieldProcessor(list);
}
}

View File

@@ -1,7 +1,19 @@
package org.dromara.common.translation.core;
import cn.hutool.core.convert.Convert;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.translation.annotation.TranslationType;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
/**
* 翻译接口 (实现类需标注 {@link TranslationType} 注解标明翻译类型)
*
@@ -17,4 +29,65 @@ public interface TranslationInterface<T> {
* @return 返回键对应的翻译值
*/
T translation(Object key, String other);
/**
* 批量翻译。
*
* @param keys 需要被翻译的键集合
* @param other 其他参数
* @return 翻译结果映射
*/
default Map<Object, T> translationBatch(Set<Object> keys, String other) {
Map<Object, T> result = new LinkedHashMap<>(keys.size());
for (Object key : keys) {
result.put(key, translation(key, other));
}
return result;
}
/**
* 收集 Long 类型键集合。
*
* @param keys 原始键集合
* @return Long 键集合
*/
default Set<Long> collectLongIds(Collection<Object> keys) {
Set<Long> result = new LinkedHashSet<>();
for (Object key : keys) {
if (key instanceof String ids) {
result.addAll(parseLongIds(ids));
} else if (key != null) {
result.add(Convert.toLong(key));
}
}
return result;
}
/**
* 解析逗号分隔的 Long ID 列表。
*
* @param ids 逗号分隔字符串
* @return Long 列表
*/
default List<Long> parseLongIds(String ids) {
return StreamUtils.toList(
StreamUtils.filter(Arrays.asList(ids.split(StringUtils.SEPARATOR)), StringUtils::isNotBlank),
value -> Convert.toLong(value.trim())
);
}
/**
* 按原始 ID 顺序拼接映射值。
*
* @param ids 原始 ID 字符串
* @param mapper ID 到值的映射函数
* @return 拼接后的结果字符串
* @param <E> 值类型
*/
default <E> String joinMappedValues(String ids, Function<Long, E> mapper) {
return StreamUtils.join(parseLongIds(ids), id -> {
E value = mapper.apply(id);
return value == null ? null : String.valueOf(value);
});
}
}

View File

@@ -1,37 +0,0 @@
package org.dromara.common.translation.core.handler;
import tools.jackson.databind.BeanDescription;
import tools.jackson.databind.SerializationConfig;
import tools.jackson.databind.ser.BeanPropertyWriter;
import tools.jackson.databind.ser.ValueSerializerModifier;
import java.util.List;
/**
* Bean 序列化修改器 解决 Null 被单独处理问题
*
* @author Lion Li
*/
public class TranslationBeanSerializerModifier extends ValueSerializerModifier {
/**
* 为翻译字段补充空值序列化器,确保字段值为 {@code null} 时仍能走翻译处理链。
*
* @param config 当前序列化配置
* @param beanDesc Bean 描述提供者
* @param beanProperties 当前 Bean 的属性写入器列表
* @return 调整后的属性写入器列表
*/
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription.Supplier beanDesc,
List<BeanPropertyWriter> beanProperties) {
for (BeanPropertyWriter writer : beanProperties) {
// 如果序列化器为 TranslationHandler 的话 将 Null 值也交给他处理
if (writer.getSerializer() instanceof TranslationHandler serializer) {
writer.assignNullSerializer(serializer);
}
}
return super.changeProperties(config, beanDesc, beanProperties);
}
}

View File

@@ -1,99 +0,0 @@
package org.dromara.common.translation.core.handler;
import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.core.TranslationInterface;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.BeanProperty;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* 翻译处理器
*
* @author Lion Li
*/
@Slf4j
public class TranslationHandler extends ValueSerializer<Object> {
/**
* 全局翻译实现类映射器
*/
public static final Map<String, TranslationInterface<?>> TRANSLATION_MAPPER = new ConcurrentHashMap<>();
private final Translation translation;
/**
* 提供给 jackson 创建上下文序列化器时使用 不然会报错
*/
public TranslationHandler() {
this.translation = null;
}
/**
* 创建绑定指定翻译注解的序列化处理器。
*
* @param translation 当前字段上声明的翻译注解
*/
public TranslationHandler(Translation translation) {
this.translation = translation;
}
/**
* 将原始字段值翻译为展示值并写回序列化结果。
*
* @param value 原始字段值
* @param gen Json 输出器
* @param ctxt 序列化上下文
* @throws JacksonException Json 序列化异常
*/
@Override
public void serialize(Object value, JsonGenerator gen, SerializationContext ctxt) throws JacksonException {
TranslationInterface<?> trans = TRANSLATION_MAPPER.get(translation.type());
if (ObjectUtil.isNotNull(trans)) {
// 如果映射字段不为空 则取映射字段的值
if (StringUtils.isNotBlank(translation.mapper())) {
value = ReflectUtils.invokeGetter(gen.currentValue(), translation.mapper());
}
// 如果为 null 直接写出
if (ObjectUtil.isNull(value)) {
gen.writeNull();
return;
}
try {
Object result = trans.translation(value, translation.other());
gen.writePOJO(result);
} catch (Exception e) {
log.error("翻译处理异常type: {}, value: {}", translation.type(), value, e);
// 出现异常时输出原始值而不是中断序列化
gen.writePOJO(value);
}
} else {
gen.writePOJO(value);
}
}
/**
* 按字段上的 {@link Translation} 注解创建上下文相关的翻译序列化器。
*
* @param ctxt 序列化上下文
* @param property 当前序列化属性
* @return 存在翻译注解时返回新的翻译处理器,否则沿用默认序列化器
*/
@Override
public ValueSerializer<?> createContextual(SerializationContext ctxt, BeanProperty property) {
Translation translation = property.getAnnotation(Translation.class);
if (Objects.nonNull(translation)) {
return new TranslationHandler(translation);
}
return super.createContextual(ctxt, property);
}
}

View File

@@ -0,0 +1,133 @@
package org.dromara.common.translation.core.handler;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.json.enhance.JsonEnhancementContext;
import org.dromara.common.json.enhance.JsonFieldContext;
import org.dromara.common.json.enhance.JsonFieldProcessor;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.annotation.TranslationType;
import org.dromara.common.translation.core.TranslationInterface;
import org.springframework.core.annotation.Order;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* 翻译响应处理器。
*/
@Slf4j
@Order(0)
@RequiredArgsConstructor
public class TranslationJsonFieldProcessor implements JsonFieldProcessor {
private static final String ATTR_BATCHES = TranslationJsonFieldProcessor.class.getName() + ".batches";
private static final String ATTR_RESULTS = TranslationJsonFieldProcessor.class.getName() + ".results";
private final List<TranslationInterface<?>> translations;
@Override
public void collect(JsonFieldContext fieldContext, JsonEnhancementContext context) {
Translation translation = fieldContext.getAnnotation(Translation.class);
if (translation == null) {
return;
}
Object sourceValue = resolveSourceValue(fieldContext, translation);
if (sourceValue == null) {
return;
}
Map<TranslationBatchKey, Set<Object>> batches = getOrCreateBatches(context);
batches.computeIfAbsent(new TranslationBatchKey(translation.type(), translation.other()), key -> new LinkedHashSet<>())
.add(sourceValue);
}
@Override
public void prepare(JsonEnhancementContext context) {
Map<TranslationBatchKey, Set<Object>> batches = context.getAttribute(ATTR_BATCHES);
if (batches == null || batches.isEmpty()) {
return;
}
Map<TranslationBatchKey, Map<Object, Object>> results = new LinkedHashMap<>(batches.size());
for (Map.Entry<TranslationBatchKey, Set<Object>> entry : batches.entrySet()) {
TranslationInterface<?> translation = getTranslation(entry.getKey().type());
if (translation == null) {
continue;
}
try {
Map<Object, ?> translated = translation.translationBatch(entry.getValue(), entry.getKey().other());
results.put(entry.getKey(), new LinkedHashMap<>(translated));
} catch (Exception e) {
log.error("批量翻译处理异常type: {}, other: {}", entry.getKey().type(), entry.getKey().other(), e);
}
}
context.setAttribute(ATTR_RESULTS, results);
}
@Override
public Object process(JsonFieldContext fieldContext, Object value, JsonEnhancementContext context) {
Translation translation = fieldContext.getAnnotation(Translation.class);
if (translation == null) {
return value;
}
Object sourceValue = resolveSourceValue(fieldContext, translation);
if (sourceValue == null) {
return null;
}
TranslationBatchKey batchKey = new TranslationBatchKey(translation.type(), translation.other());
Map<TranslationBatchKey, Map<Object, Object>> results = context.getAttribute(ATTR_RESULTS);
if (results != null) {
Map<Object, Object> translatedMap = results.get(batchKey);
if (translatedMap != null && translatedMap.containsKey(sourceValue)) {
return translatedMap.get(sourceValue);
}
}
TranslationInterface<?> trans = getTranslation(translation.type());
if (ObjectUtil.isNull(trans)) {
return value;
}
try {
return trans.translation(sourceValue, translation.other());
} catch (Exception e) {
log.error("翻译处理异常type: {}, value: {}", translation.type(), sourceValue, e);
return value;
}
}
private Map<TranslationBatchKey, Set<Object>> getOrCreateBatches(JsonEnhancementContext context) {
Map<TranslationBatchKey, Set<Object>> batches = context.getAttribute(ATTR_BATCHES);
if (batches == null) {
batches = new LinkedHashMap<>();
context.setAttribute(ATTR_BATCHES, batches);
}
return batches;
}
private Object resolveSourceValue(JsonFieldContext fieldContext, Translation translation) {
if (StringUtils.isNotBlank(translation.mapper())) {
return ReflectUtils.invokeGetter(fieldContext.owner(), translation.mapper());
}
return fieldContext.value();
}
private TranslationInterface<?> getTranslation(String type) {
for (TranslationInterface<?> translation : translations) {
TranslationType translationType = translation.getClass().getAnnotation(TranslationType.class);
if (translationType != null && Objects.equals(type, translationType.type())) {
return translation;
}
}
return null;
}
private record TranslationBatchKey(String type, String other) {
}
}

View File

@@ -1,10 +1,15 @@
package org.dromara.common.translation.core.impl;
import cn.hutool.core.convert.Convert;
import lombok.AllArgsConstructor;
import org.dromara.common.core.service.DeptService;
import org.dromara.common.translation.annotation.TranslationType;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.common.translation.core.TranslationInterface;
import lombok.AllArgsConstructor;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* 部门翻译实现
@@ -33,4 +38,25 @@ public class DeptNameTranslationImpl implements TranslationInterface<String> {
}
return null;
}
@Override
public Map<Object, String> translationBatch(Set<Object> keys, String other) {
Set<Long> deptIds = collectLongIds(keys);
if (deptIds.isEmpty()) {
return Map.of();
}
Map<Long, String> deptNames = deptService.selectDeptNamesByIds(deptIds);
Map<Object, String> result = new LinkedHashMap<>(keys.size());
for (Object key : keys) {
result.put(key, buildValue(key, deptNames));
}
return result;
}
private String buildValue(Object source, Map<Long, String> deptNames) {
if (source instanceof String ids) {
return joinMappedValues(ids, deptNames::get);
}
return source == null ? null : deptNames.get(Convert.toLong(source));
}
}

View File

@@ -1,11 +1,17 @@
package org.dromara.common.translation.core.impl;
import lombok.AllArgsConstructor;
import org.dromara.common.core.service.DictService;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.translation.annotation.TranslationType;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.common.translation.core.TranslationInterface;
import lombok.AllArgsConstructor;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* 字典翻译实现
@@ -32,4 +38,22 @@ public class DictTypeTranslationImpl implements TranslationInterface<String> {
}
return null;
}
@Override
public Map<Object, String> translationBatch(Set<Object> keys, String other) {
if (keys.isEmpty() || StringUtils.isBlank(other)) {
return Map.of();
}
Map<String, String> dictMap = dictService.getAllDictByDictType(other);
Map<Object, String> result = new LinkedHashMap<>(keys.size());
for (Object key : keys) {
if (key instanceof String dictValue) {
result.put(key, StreamUtils.join(
StreamUtils.filter(Arrays.asList(dictValue.split(",")), StringUtils::isNotBlank),
value -> dictMap.get(value.trim())
));
}
}
return result;
}
}

View File

@@ -1,11 +1,16 @@
package org.dromara.common.translation.core.impl;
import cn.hutool.core.convert.Convert;
import lombok.AllArgsConstructor;
import org.dromara.common.core.service.UserService;
import org.dromara.common.translation.annotation.TranslationType;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.common.translation.core.TranslationInterface;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* 用户昵称翻译实现
*
@@ -33,4 +38,25 @@ public class NicknameTranslationImpl implements TranslationInterface<String> {
}
return null;
}
@Override
public Map<Object, String> translationBatch(Set<Object> keys, String other) {
Set<Long> userIds = collectLongIds(keys);
if (userIds.isEmpty()) {
return Map.of();
}
Map<Long, String> userNames = userService.selectUserNicksByIds(userIds);
Map<Object, String> result = new LinkedHashMap<>(keys.size());
for (Object key : keys) {
result.put(key, buildValue(key, userNames));
}
return result;
}
private String buildValue(Object source, Map<Long, String> userNames) {
if (source instanceof String ids) {
return joinMappedValues(ids, userNames::get);
}
return source == null ? null : userNames.get(Convert.toLong(source));
}
}

View File

@@ -1,10 +1,18 @@
package org.dromara.common.translation.core.impl;
import cn.hutool.core.convert.Convert;
import lombok.AllArgsConstructor;
import org.dromara.common.core.domain.dto.OssDTO;
import org.dromara.common.core.service.OssService;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.translation.annotation.TranslationType;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.common.translation.core.TranslationInterface;
import lombok.AllArgsConstructor;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* OSS翻译实现
@@ -33,4 +41,26 @@ public class OssUrlTranslationImpl implements TranslationInterface<String> {
}
return null;
}
@Override
public Map<Object, String> translationBatch(Set<Object> keys, String other) {
Set<Long> ossIds = collectLongIds(keys);
if (ossIds.isEmpty()) {
return Map.of();
}
String idText = ossIds.stream().map(String::valueOf).collect(Collectors.joining(","));
Map<Long, String> ossUrls = new LinkedHashMap<>(StreamUtils.toMap(ossService.selectByIds(idText), OssDTO::getOssId, OssDTO::getUrl));
Map<Object, String> result = new LinkedHashMap<>(keys.size());
for (Object key : keys) {
result.put(key, buildValue(key, ossUrls));
}
return result;
}
private String buildValue(Object source, Map<Long, String> ossUrls) {
if (source instanceof String ids) {
return joinMappedValues(ids, ossUrls::get);
}
return source == null ? null : ossUrls.get(Convert.toLong(source));
}
}

View File

@@ -1,11 +1,17 @@
package org.dromara.common.translation.core.impl;
import cn.hutool.core.convert.Convert;
import lombok.AllArgsConstructor;
import org.dromara.common.core.domain.dto.UserDTO;
import org.dromara.common.core.service.UserService;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.translation.annotation.TranslationType;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.common.translation.core.TranslationInterface;
import lombok.AllArgsConstructor;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* 用户名翻译实现
@@ -29,4 +35,25 @@ public class UserNameTranslationImpl implements TranslationInterface<String> {
public String translation(Object key, String other) {
return userService.selectUserNameById(Convert.toLong(key));
}
@Override
public Map<Object, String> translationBatch(Set<Object> keys, String other) {
Set<Long> userIds = collectLongIds(keys);
if (userIds.isEmpty()) {
return Map.of();
}
Map<Long, String> userNames = new LinkedHashMap<>(StreamUtils.toMap(userService.selectListByIds(userIds), UserDTO::getUserId, UserDTO::getUserName));
Map<Object, String> result = new LinkedHashMap<>(keys.size());
for (Object key : keys) {
result.put(key, buildValue(key, userNames));
}
return result;
}
private String buildValue(Object source, Map<Long, String> userNames) {
if (source instanceof String ids) {
return joinMappedValues(ids, userNames::get);
}
return userNames.get(Convert.toLong(source));
}
}