注解中动态参数的处理
Java 工程中注解配合切面往往能带来事半功倍的效果,但是注解有的时候并不是太灵活,例如方法注解无法指定一些自定义的参数,很多时候还是需要到方法里面去做一些侵入性的逻辑来完成想要的操作;为了解决这个痛点,我写了个 demo 通过反射+递归的方式来给方法注解提供解析动态参数的能力,例如对于 void doSomething(Entity entity)
这个方法,可以通过 @RecordEvent(code = "TEST", keyWord = "{entity.id}", content = "content is {entity.nested.foo}")
注解中的花括号来取到入参中 entity
参数的某些值,且支持无限递归获取子属性的值,demo 代码如下:
有这么两个参数类型,其中Entity类中包含类型为Nested的属性
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Entity {
private int id;
private String name;
private String otherField;
private Nested nested;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Nested {
private String foo;
private int bar;
}
有一个方法,其第一个入参名为entity,可以使用“{entity.nested.foo}”这样的模板字符串,切面处理时会把模板字符串自动替换为对应入参属性的getter值,支持无限级往下查属性,查到最后的属性(本例中为foo)将其toString替换到模板。
@Override
@RecordEvent(code = "TEST", keyWord = "{entity.id}", content = "content is {entity.nested.foo}")
public void doSomething(Entity entity) {
int a = 2*2;
// some other logic
log.info("done");
}
public static void main() {
Entity entity = new Entity(1, "hello", "world", new Nested("bar", 111));
xxxService.doSomething(entity);
}
调用时生成事件:EventPublishRequestDto(source=OPS, code=TEST, keyWord=1, content=content is bar, dateTime=2021-07-27T17:06:19.593)
具体方法是将模板参数用分隔符分割成队列,队列的第一个值是入参名,第二个值往后依次出队递归拿到文本对应的属性,以此类推直到队列为空,递归结束返回最深层的值
demo 源码如下:
注解:
/**
* @author syf
* @create 2021-07-22
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordEvent {
/**
* 事件编码
* @return
*/
String code();
/**
* 关键字
* @return
*/
String keyWord();
/**
* 事件内容
* 默认为空时填充:触发{Class.Method}
* @return
*/
String content() default "";
/**
* 获取模板参数值时是否使用缓存
* 建议在性能和实时性要求高时使用缓存,缓存map会占用jvm内存
* @return
*/
boolean useReflectCache() default false;
}
切面:
/**
* @author syf
* @create 2021-07-22
**/
@Aspect
@Component
@Slf4j
public class RecordEventHandler {
@Resource
private EbcEventPublishService ebcEventPublishService;
private ExecutorService executorService;
@Value("${spring.application.name:UNKNOWN}")
private String appName;
private String sysCode;
@PostConstruct
void init() {
appName = appName.toUpperCase();
sysCode = appName.endsWith("-MS") ? appName.replace("-MS", "") : appName;
int corePoolSize = 20;
int maxPoolSIze = 200;
int maxQueueSize = 500;
long keepAliveTime = 60L;
String metricLabel = "event";
executorService = new ThreadPoolExecutor(
corePoolSize, maxPoolSIze, keepAliveTime, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(maxQueueSize), new BaseThreadFactory(metricLabel));
}
/**
* 切面,切@RecordEvent注解,异步处理事件发送
* @param joinPoint
* @param recordEvent
* @return
* @throws Throwable
*/
@Around("@annotation(recordEvent)")
public Object recordEventHandler(ProceedingJoinPoint joinPoint, RecordEvent recordEvent) throws Throwable {
try {
CompletableFuture.supplyAsync(() -> recordEventProcessor(joinPoint, recordEvent), executorService)
.whenComplete((result, e) -> {
if (null != e) {
log.error("recordEventHandler event send failed, ", e);
} else if (!result.equals(-1L)) {
log.info("recordEventHandler event send success, event id: {}", result);
}
});
} catch (Exception e) {
log.error("recordEventHandler event send failed, ", e);
}
return joinPoint.proceed();
}
/**
* 渲染参数,发送事件逻辑
* @param joinPoint
* @param recordEvent
* @return
*/
private Long recordEventProcessor(ProceedingJoinPoint joinPoint, RecordEvent recordEvent) {
Object[] argValues = joinPoint.getArgs();
CodeSignature signature = (CodeSignature) joinPoint.getSignature();
String[] argNames = signature.getParameterNames();
if (argNames.length != argValues.length) {
log.warn("argNames.length != argValues.length, names: {}, values: {}", argNames, argValues);
}
Map<String, Object> args = new HashMap<>();
for (int i = 0; i < argNames.length; i++) {
args.put(argNames[i], argValues[i]);
}
boolean useCache = recordEvent.useReflectCache();
String source = sysCode;
String code = recordEvent.code();
String keyWord = parseTemplate(recordEvent.keyWord(), args, useCache);
String content = parseTemplate(recordEvent.content(), args, useCache);
if (StringUtils.isEmpty(content)) {
// content为空使用method填充
content = "触发" + signature.getName() + "方法";
}
EventPublishRequestDto eventPublishRequestDto = new EventPublishRequestDto(source, code, keyWord, content);
log.info("triggered event publish: {}", eventPublishRequestDto);
return ebcEventPublishService.publish(eventPublishRequestDto);
}
/**
* 替换模板文本中的参数
* @param template
* @param args
* @param useCache
* @return
*/
private String parseTemplate(String template, Map<String, Object> args, boolean useCache) {
Map<String, String> templateFields = parseTemplateFields(template, args, useCache);
if (MapUtils.isEmpty(templateFields)) {
return template;
}
for (Map.Entry<String, String> fieldValue: templateFields.entrySet()) {
template = template.replace("{" + fieldValue.getKey() + "}", fieldValue.getValue());
}
return template;
}
/**
* 根据入参和模板字符串解析模板参数的值
* @param template 模板字符串,如hello{foo.bar}world
* @param args 入参
* @param useCache 解析过程是否使用反射缓存
* @return Map<foo.bar, barValue>
*/
private Map<String, String> parseTemplateFields(String template, Map<String, Object> args, boolean useCache) {
List<String> fields2Parse = parseTemplateFieldNames(template);
if (CollectionUtils.isEmpty(fields2Parse) || MapUtils.isEmpty(args)) {
return null;
}
return fields2Parse.stream().collect(Collectors.toMap(Function.identity(), field -> {
Queue<String> locator = new LinkedList<>(Arrays.asList(field.split("\\.")));
String argName = locator.poll(); // 第一个值是入参名,单独处理
// 先找到argName,再递归找到最底层属性getter值
if (args.containsKey(argName)) {
Object entity = args.get(argName);
// 递归入口
return ReflectUtil.getFieldValueRecursively(entity, locator, useCache);
}
return "null";
}));
}
/**
* parse dynamic field like "aaa{entity.name}, {foo.bar}bbb" to ["entity.name", "foo.bar"]
* @param template
* @return ["entity.field1", "entity.field2"]
*/
private List<String> parseTemplateFieldNames(String template) {
if (StringUtils.isBlank(template) || !(template.contains("{") && template.contains("}"))) {
return Collections.emptyList();
}
List<String> fields = new LinkedList<>();
int length = template.length();
for (int start = 0; start < length; start++) {
// find start '{'
if (template.charAt(start) == '{') {
int end = start;
// find end '}'
while (end < length && template.charAt(end) != '}') {
end++;
}
// found end '}' and field is not empty "{}"
if (end != length && end != start+1) {
fields.add(template.substring(start+1, end));
start = end + 1; // fast move start pos to next position
}
}
}
return fields;
}
}
反射工具集:
/**
* 利用递归和反射获取
* @author syf
* @create 2021-07-27
**/
@Getter
@Setter
@Slf4j
public class ReflectUtil {
private ReflectUtil() {}
/**
* key: Pair<Nested.class, field>
* value: Entity.Nested.getFoo()
*/
private static Map<Pair<Class<?>, String>, Method> reflectGetterMethodsMap;
/**
* 反射获取属性getter,加载一次后存缓存
* @param field 属性
* @return getter方法
*/
private static Method loadGetter(Class<?> clazz, String field, boolean useCache) {
Pair<Class<?>, String> classField = ImmutablePair.of(clazz, field);
if (useCache && reflectGetterMethodsMap.containsKey(classField)) {
return reflectGetterMethodsMap.get(classField);
} else {
String methodName = "get" + field.substring(0, 1).toUpperCase() + field.substring(1);
try {
Method getter = clazz.getMethod(methodName);
if (useCache) {
reflectGetterMethodsMap.put(classField, getter);
}
return getter;
} catch (NoSuchMethodException e) {
log.error("no such field getter: {} in class: {}", methodName, clazz, e);
return null;
}
}
}
/**
* 递归获取对最底层field的getter值
* return field getter value of an object,
* support nested field like 'object.nested.foo',
* where 'nested' can be any class.
* 例如entity.someNested.foo
* @param entity Entity entity
* @param locator ["someNested", "foo"]
* @param useCache 是否使用反射方法缓存
* @return 最底层field的getter值 if success else "null"
*/
public static String getFieldValueRecursively(Object entity, Queue<String> locator, boolean useCache) {
if (locator.isEmpty()) {
// locator队列为空说明所有属性都解析完了,返回当前object string值
return entity.toString();
} else {
try {
// 从locator队列中poll取出一个field getter并执行,得到fieldValue
Method getter = loadGetter(entity.getClass(), locator.poll(), useCache);
if (getter != null) {
Object fieldValue = getter.invoke(entity);
return getFieldValueRecursively(fieldValue, locator, useCache);
}
} catch (Exception e) {
log.error("invoke getter error, cause: ", e);
}
log.error("getFieldValue failed, returned 'null'");
return "null";
}
}
}