运维页面

This commit is contained in:
2026-08-20 21:52:13 +08:00
parent ecc217ba86
commit b4b660f60d
2 changed files with 1308 additions and 0 deletions
@@ -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<Class<?>> 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<Enum>) 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());
}
}
File diff suppressed because it is too large Load Diff