diff --git a/src/main/java/com/yunhai/bschclassmanage/utils/ReflectRetryUtil.java b/src/main/java/com/yunhai/bschclassmanage/utils/ReflectRetryUtil.java new file mode 100644 index 0000000..3d4ce0d --- /dev/null +++ b/src/main/java/com/yunhai/bschclassmanage/utils/ReflectRetryUtil.java @@ -0,0 +1,221 @@ +package com.yunhai.bschclassmanage.utils; + +import com.alibaba.fastjson.JSON; +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.util.ClassUtils; + +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.security.Principal; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +/** + * 重试反射重放工具:根据审计/同步日志中记录的 classMethod + 入参, + * 通过 Spring Bean 反射重放原始调用。代理安全(支持 CGLIB / JDK 动态代理)。 + */ +public final class ReflectRetryUtil { + + private ReflectRetryUtil() { + } + + /** + * 无法由日志重建、重放时须置 null 的参数类型(servlet / Spring MVC 相关)。 + * 这些参数原本由容器注入,日志记录时已过滤,重放时无法还原。 + */ + private static final List> SKIP_TYPES = Arrays.asList( + javax.servlet.http.HttpServletRequest.class, + javax.servlet.http.HttpServletResponse.class, + javax.servlet.http.HttpSession.class, + org.springframework.ui.Model.class, + org.springframework.ui.ModelMap.class, + org.springframework.web.servlet.ModelAndView.class, + org.springframework.validation.BindingResult.class, + org.springframework.validation.Errors.class, + org.springframework.web.bind.support.SessionStatus.class, + Principal.class, + Locale.class, + InputStream.class, + OutputStream.class + ); + + /** 该参数类型是否应在重放时置 null(容器注入、无法由日志重建) */ + public static boolean isSkippable(Class type) { + if (type == null) return false; + for (Class s : SKIP_TYPES) { + if (s.isAssignableFrom(type)) return true; + } + return false; + } + + /** 去掉 CGLIB 代理类名后缀($$EnhancerBySpringCGLIB$$...),还原原始类名 */ + public static String stripProxySuffix(String className) { + if (className == null) return null; + int cglib = className.indexOf("$$"); + if (cglib > 0) return className.substring(0, cglib); + return className; // JDK 代理($ProxyN) 原样返回,交由 bean 扫描兜底 + } + + /** 定位结果:目标 Bean + 其上的原始 Method */ + public static final class Resolved { + public final Object bean; + public final Method method; + + public Resolved(Object bean, Method method) { + this.bean = bean; + this.method = method; + } + } + + /** + * 根据 classMethod(类名.方法名) 与「可重建入参个数」定位 Spring Bean + Method。 + * 优先按原始类直接 getBean;失败(JDK 代理 / 类名丢失)则扫描所有 Bean, + * 按「类名(去包)匹配 + 方法名 + 有效参数个数」唯一确定。 + * + * @param effectiveArgCount 非 servlet/容器注入参数的个数(与日志中入参个数一致) + */ + public static Resolved resolveBeanAndMethod(ApplicationContext ctx, String classMethod, int effectiveArgCount) { + if (classMethod == null || classMethod.lastIndexOf('.') < 0) { + throw new IllegalArgumentException("无效的 classMethod:" + classMethod); + } + int dot = classMethod.lastIndexOf('.'); + String className = classMethod.substring(0, dot); + String methodName = classMethod.substring(dot + 1); + String stripped = stripProxySuffix(className); + + // 1) 按原始类直接定位(CGLIB 场景最常见) + try { + Class clazz = Class.forName(stripped); + Object bean = ctx.getBean(clazz); + Method m = findMethod(clazz, methodName, effectiveArgCount); + if (m != null) return new Resolved(bean, m); + } catch (Throwable ignored) { + // 类名解析失败 / bean 不存在 → 走扫描兜底 + } + + // 2) 扫描 Bean 兜底(JDK 动态代理 / 类名不完整) + String simpleName = stripped.substring(stripped.lastIndexOf('.') + 1); + Resolved best = null; + int matchCount = 0; + for (String name : ctx.getBeanDefinitionNames()) { + Object bean; + try { + bean = ctx.getBean(name); + } catch (Throwable t) { + continue; + } + if (bean == null) continue; + Class userClass = ClassUtils.getUserClass(AopUtils.getTargetClass(bean)); + if (!userClass.getSimpleName().equals(simpleName)) continue; + Method m = findMethod(userClass, methodName, effectiveArgCount); + if (m != null) { + matchCount++; + best = new Resolved(bean, m); + } + } + if (best != null && matchCount == 1) return best; + if (best != null) { + throw new com.yunhai.platform.utils.exception.DException("匹配到多个候选 Bean(" + methodName + "),无法唯一确定,请检查 classMethod"); + } + throw new com.yunhai.platform.utils.exception.DException("未找到可重试的目标方法:" + classMethod + " (有效入参数=" + effectiveArgCount + ")"); + } + + /** + * 在 clazz 上按「方法名 + 有效参数个数」定位唯一方法。 + * 有效参数个数 = 排除 servlet/容器注入参数后的个数(与日志入参个数一致)。 + */ + public static Method findMethod(Class clazz, String methodName, int effectiveArgCount) { + for (Method m : clazz.getMethods()) { + if (!m.getName().equals(methodName)) continue; + int eff = 0; + for (Parameter p : m.getParameters()) { + if (!isSkippable(p.getType())) eff++; + } + if (eff == effectiveArgCount) return m; + } + return null; + } + + /** + * 取得可用于反射调用的 Method:优先用代理 Bean 自身的方法对象, + * 失败则回退到原始 Method(覆盖 JDK 代理泛型擦除等边界情况)。 + */ + public static Method getInvokeMethod(Object bean, Method target) { + try { + return bean.getClass().getMethod(target.getName(), target.getParameterTypes()); + } catch (NoSuchMethodException e) { + for (Method m : bean.getClass().getMethods()) { + if (m.getName().equals(target.getName()) + && m.getParameterCount() == target.getParameterCount()) { + return m; + } + } + return target; + } + } + + /** + * 将日志中存储的入参值 raw 转换为目标参数类型 param。 + * 支持:基本类型/包装、String、枚举、List/Collection、POJO。 + */ + public static Object convert(Object raw, Parameter param) { + if (raw == null) return null; + Class type = param.getType(); + if (type.isAssignableFrom(raw.getClass())) return raw; + if (type == String.class) return raw.toString(); + if (type == Integer.class || type == int.class) { + return raw instanceof Number ? ((Number) raw).intValue() : Integer.valueOf(raw.toString().trim()); + } + if (type == Long.class || type == long.class) { + return raw instanceof Number ? ((Number) raw).longValue() : Long.valueOf(raw.toString().trim()); + } + if (type == Double.class || type == double.class) { + return raw instanceof Number ? ((Number) raw).doubleValue() : Double.valueOf(raw.toString().trim()); + } + if (type == Float.class || type == float.class) { + return raw instanceof Number ? ((Number) raw).floatValue() : Float.valueOf(raw.toString().trim()); + } + if (type == Boolean.class || type == boolean.class) { + return Boolean.parseBoolean(raw.toString().trim()); + } + if (type.isEnum()) { + return Enum.valueOf((Class) type, raw.toString().trim()); + } + if (List.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type)) { + Type elemType = Object.class; + if (param.getParameterizedType() instanceof ParameterizedType) { + Type[] actual = ((ParameterizedType) param.getParameterizedType()).getActualTypeArguments(); + if (actual.length > 0) elemType = actual[0]; + } + if (elemType == Integer.class) return JSON.parseArray(raw.toString(), Integer.class); + if (elemType instanceof Class) return JSON.parseArray(raw.toString(), (Class) elemType); + return JSON.parseArray(raw.toString()); + } + // 其余按 POJO 反序列化 + return JSON.parseObject(raw.toString(), type); + } + + /** 统一把反射调用异常转译为运行时异常(DException 透传) */ + public static RuntimeException rethrow(Throwable ex) { + if (ex instanceof InvocationTargetException) { + Throwable cause = ((InvocationTargetException) ex).getCause(); + if (cause instanceof com.yunhai.platform.utils.exception.DException) + throw (com.yunhai.platform.utils.exception.DException) cause; + if (cause instanceof RuntimeException) throw (RuntimeException) cause; + throw new com.yunhai.platform.utils.exception.DException( + cause != null ? cause.getMessage() : "重试执行失败"); + } + if (ex instanceof com.yunhai.platform.utils.exception.DException) + throw (com.yunhai.platform.utils.exception.DException) ex; + if (ex instanceof RuntimeException) throw (RuntimeException) ex; + throw new com.yunhai.platform.utils.exception.DException("重试失败:" + ex.getMessage()); + } +} diff --git a/src/main/resources/static/work-view.html b/src/main/resources/static/work-view.html new file mode 100644 index 0000000..4e31bbe --- /dev/null +++ b/src/main/resources/static/work-view.html @@ -0,0 +1,1087 @@ + + + + + + 运维工作台 + + + +
+
运维工作台:接口日志 / 同步日志 / 缓存管理。仅超级管理员可访问,部署后同源访问自动带登录态。
+
+ +
+
接口日志
+
同步日志
+
缓存管理
+
+ + +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+ + +
+
+ + + + + + + + +
ID级别操作类型URI方法状态码耗时操作人来源IP时间操作
点击「查询」加载数据
+
+
+ + + + + + +
+
+
+ + +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + +
+
+ + + + + + + + +
ID大类同步类型类名.方法名状态操作人来源IP时间操作
点击「查询」加载数据
+
+
+ + + + + + +
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
生产大库下 KEYS 会阻塞,请尽量指定明确前缀,勿用单独的 *。
+
+
+
+ +
+
+
输入 Key 或列出 Keys 后点击查询,结果将显示在此处。
+ +
+
+
+
+ + + + +
+ + + +