此文章基于我之前写的异常信息记录,详情参阅使用aop切面记录程序接口异常日志信息,包含完整方法名,参数类型,参数名,参数值及接口信息,异常信息_影耀子的博客-优快云博客
首先调用自动执行异常日志的接口,为了简化开发,使用了mybatisplus框架
/**
* 自动执行异常日志记录的接口重试,
* EXCEPTION_LOG_VOLUNTARILY
* @param id
* @return
*/
@GetMapping(value = "voluntarily")
public R voluntarily(Integer id){
try {
ExceptionLog exceptionLogById = exceptionLogService.getById(id);
JSONObject jsonParameter = JSONUtil.parseObj(exceptionLogById.getParameter());
JSONArray jsonArray = jsonParameter.getJSONArray(ResolveExceptionUtil.getParameterArrayKey());
StringBuilder parameter = new StringBuilder();
for (Object o : jsonArray) {
parameter.append("\"").append(o).append("\",");
}
parameter.setLength(parameter.length()-1);
String parameterStr = parameter.toString();
int num = 100;
Integer idGt = 0;
List<Integer> exceptionLogIds = new ArrayList<>();
while (true){
List<ExceptionLog> exceptionLogList = exceptionLogService.findExceptionLogByMethodAndJsonParameter(exceptionLogById,idGt,num,parameterStr);
if (exceptionLogList.isEmpty()){
break;
}
idGt = exceptionLogList.get(exceptionLogList.size()-1).getId();
// 重新执行成功的信息
exceptionLogIds.addAll(VoluntarilyRetryException.voluntarilyRetry(exceptionLogList));
}
return new R(RCode.SUCCESS, exceptionLogIds);
} catch (Exception e) {
// 捕获此接口是因为执行此接口产生的异常不加入异常日志
e.printStackTrace();
}
return R.FAIL();
}
exceptionLogService.findExceptionLogByMethodAndJsonParameter()方法代码如下
此方法的作用是根据id将异常日志表中记录的此方法中的异常信息全部查询出来,使用了mysql8.0以上版本的json函数
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lookvin.admin.config.entity.ExceptionLog;
import com.lookvin.admin.config.mapper.ExceptionLogMapper;
import com.lookvin.admin.config.service.IExceptionLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
/**
* <p>
* 异常日志表 服务实现类
* </p>
*
* @author YingYew
* @since 2022-08-02
*/
@Service
@Transactional
public class ExceptionLogServiceImpl extends ServiceImpl<ExceptionLogMapper, ExceptionLog> implements IExceptionLogService {
@Autowired
private ExceptionLogMapper exceptionLogMapper;
@Override
public List<ExceptionLog> findExceptionLogByMethodAndJsonParameter(ExceptionLog exceptionLogById, Integer id,Integer num,String parameter) {
return new QueryChainWrapper<>(exceptionLogMapper)
.gt("id",id)
.eq("method",exceptionLogById.getMethod())
.apply("parameter->'$.\""+ ResolveExceptionUtil.getParameterArrayKey() +
"\"' = json_array(" + parameter +
")")
.orderByAsc("id")
.last("limit "+num)
.list();
}
}
VoluntarilyRetryException.voluntarilyRetry()方法代码如下:
此方法就是将那些异常信息模仿用户操作重新执行,然后返回执行成功的id
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.lookvin.admin.config.entity.ExceptionLog;
import com.lookvin.common.util.jwt.JwtUtil;
import com.lookvin.common.util.requestUrl.RequestUtil;
import com.lookvin.common.util.user.UserCookie;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
/**
* @program: newlookvin-parent
* @description 自动执行异常日志,相当于模拟用户重新调用相关接口
* @author: 影耀子(YingYew)
* @create: 2023-04-24 17:44
**/
public class VoluntarilyRetryException {
/**
* 自动执行异常日志,相当于模拟用户重新调用相关接口
* @param exceptionLogList
* @return
*/
public static List<Integer> voluntarilyRetry(List<ExceptionLog> exceptionLogList){
List<Integer> exceptionLogListIds = new ArrayList<>();
Map<String,Method> methodMap = new HashMap<>();
// 需要重试的异常信息
try {
for (ExceptionLog exceptionLog : exceptionLogList) {
String method = exceptionLog.getMethod();
Map<String, Object> map = new HashMap<>();
if (method.contains(".controller.")){
// 代表管理后台的接口
UserCookie.admin(map,exceptionLog.getOperatingId());
}else {
// 代表用户后台或者用户前台或其他的接口
UserCookie.user(map,exceptionLog.getOperatingId());
}
// 模拟用户的token信息
String authorization = JwtUtil.authorizationPrefix+JwtUtil.createJWT(exceptionLog.getOperatingId().toString(),exceptionLog.getOperating(),map);
map.put("ip",exceptionLog.getIp());
map.put("createDate",exceptionLog.getCreateDate());
// map.put("source",exceptionLog.getSource()); // 设置在请求头headerSoure中即可
// map.put("sourceUrl",exceptionLog.getSourceUrl()); // 设置在请求头RefererUrl即可
// 记录的用户操作的相关信息,用于添加ip,用户当时操作时的时间
String userInformation = JwtUtil.createJWT(exceptionLog.getOperatingId().toString(),
exceptionLog.getOperating(),map);
// 通过反射缓存方法method,返回缓存的key
String key = getMethod(methodMap,exceptionLog.getMethod(), JSONUtil.parseObj(exceptionLog.getParameter()));
boolean isSuccess = sendHttp(exceptionLog, authorization, userInformation, methodMap.get(key));
if (isSuccess){
exceptionLogListIds.add(exceptionLog.getId());
// 执行字符串sql语句,你可以使用自己的,也可以参考我这篇文章https://blog.youkuaiyun.com/yaoyipeng/article/details/129463243?spm=1001.2014.3001.5501
HutoolDbUtil.execute("delete from config_exception_log where id = ?",exceptionLog.getId());
}
}
} catch (Exception e) {
// 捕获此异常是因为要将重新执行成功的那些异常信息删除
e.printStackTrace();
}
return exceptionLogListIds;
}
/**
* 根据反射获取方法
* @param methodMap 将获取导的方法保存至mao中
* @param methodStr 字符串方法名
* @param parameter 参数类型,名,值
*/
public static String getMethod(Map<String, Method> methodMap, String methodStr, JSONObject parameter) throws ClassNotFoundException {
JSONArray parameterJSONArray = parameter.getJSONArray(ResolveExceptionUtil.getParameterArrayKey());
Class<?>[] parameterTypes = new Class[parameterJSONArray.size()];
int i = 0;
StringBuilder keyStringBuilder = new StringBuilder(methodStr);
for (Object param : parameterJSONArray) {
Class<?> aClass = Class.forName(StrUtil.subBefore(param.toString(), ".", true));
keyStringBuilder.append(",").append(aClass.getName());
parameterTypes[i] = aClass;
i++;
}
String key = keyStringBuilder.toString();
if (methodMap.get(key) == null){
// 相应的类名
String clasString = StrUtil.subBefore(methodStr,".",true);
// 方法名
String methodName = StrUtil.subAfter(methodStr,".",true);
Method method = ReflectUtil.getMethod(Class.forName(clasString), methodName,parameterTypes);
methodMap.put(key,method);
}
return key;
}
/**
* 根据异常信息发送http请求
* @param exceptionLog 异常日志表的信息
* @param authorization 用户token
* @param userInformation 记录的用户操作的相关信息,用于添加ip,用户当时操作时的时间
* @param method 根据反射得到要请求的方法,用于判断参数中请求body和form信息
* @return
*/
private static boolean sendHttp(ExceptionLog exceptionLog, String authorization,
String userInformation,Method method) {
// 获取头信息
Map<String,String> headers = getHeaders(exceptionLog,authorization,userInformation);
// 获取请求body中信息
JSONObject parameters = JSONUtil.parseObj(exceptionLog.getParameter());
// 获取请求体form信息
StringBuilder body = new StringBuilder();
Map<String,Object> formParams = getFormAndBody(parameters,method, body);
// 存储返回的json字符串
JSONObject response;
// 获取请求类型get,post,delete,put等
HttpRequest req = getApiType(exceptionLog.getApiType(),exceptionLog.getApi());
response = JSONUtil.parseObj(
req.addHeaders(headers)
.body(body.toString())
.form(formParams)
// .setHttpProxy("127.0.0.1",9000) // 一般不用设置主机端口
.execute().body()
);
return response.getBool("success");
}
/**
* 获取请求form及body信息
* @param parameters
* @param method
* @param body
* @return
*/
private static Map<String, Object> getFormAndBody(JSONObject parameters, Method method,StringBuilder body) {
Map<String, Object> formParams = new HashMap<>();
Class<?>[] parameterTypes = method.getParameterTypes();
Parameter[] methodParameters = method.getParameters();
int parameterCount = method.getParameterCount();
// 参数
// JSONObject jsonParam = new JSONObject();
for (int i = 0; i < parameterCount; i++) {
Class<?> parameterType = parameterTypes[i];
Parameter methodParameter = methodParameters[i];
String jsonKey = parameterType.getName() + "." + methodParameter.getName();
if (methodParameter.isAnnotationPresent(PathVariable.class)){
continue;
}
if (methodParameter.isAnnotationPresent(RequestBody.class)){
// 一般开发中一个接口只会使用一个@RequestBody注解
body.append(parameters.getJSONObject(jsonKey).toString());
continue;
}
RequestParam requestParam = methodParameter.getAnnotation(RequestParam.class);
if (requestParam != null){
if (StringUtils.isNotBlank(requestParam.name())){
// 放入请求form中
formParams.put(requestParam.name(),parameters.get(jsonKey));
}else {
// 放入请求form中
formParams.put(methodParameter.getName(),parameters.get(jsonKey));
}
continue;
}
Object param = parameters.get(jsonKey);
if (param == null){
continue;
}
if (JSONUtil.isTypeJSONObject(param.toString())){
// 如果是json类型,代表是对象
JSONObject jsonObject = JSONUtil.parseObj(param);
// 将所有的json存储到请求form中
for (Map.Entry<String, Object> stringObjectEntry : jsonObject) {
formParams.put(stringObjectEntry.getKey(),stringObjectEntry.getValue());
}
}else {
// 放入请求form中
formParams.put(methodParameter.getName(),parameters.get(jsonKey));
}
}
return formParams;
}
/**
* 获取请求的头信息
* @param exceptionLog
* @param authorization
* @param userInformation
* @return
*/
private static Map<String, String> getHeaders(ExceptionLog exceptionLog, String authorization, String userInformation) {
Map<String,String> headers = new HashMap<>();
headers.put("headerSoure",exceptionLog.getSource());
headers.put("RefererUrl",exceptionLog.getSourceUrl());
headers.put("Authorization",authorization);
headers.put(RequestUtil.EXCEPTION_LOG_TOKEN,userInformation);
return headers;
}
/**
* 获取请求类型
* @param type
* @param url
* @return
*/
public static HttpRequest getApiType(String type, String url) {
if ("PUT".equals(type)){
return HttpRequest.of(url).setMethod(cn.hutool.http.Method.PUT);
}else if ("POST".equals(type)){
return HttpRequest.of(url).setMethod(cn.hutool.http.Method.POST);
}else if ("DELETE".equals(type)){
return HttpRequest.of(url).setMethod(cn.hutool.http.Method.DELETE);
}
return HttpRequest.of(url).setMethod(cn.hutool.http.Method.GET);
}
}
userCookie代码如下,因为需要和登录时生成的token一致,所以封装了一个类,这样登录时的token数据改了,就不用改两个地方了,下图是登录时生成的token
import java.util.Map;
/**
* @program: newlookvin-parent
* @description 记录用户登录时的标识等信息
* @author: 影耀子(YingYew)
* @create: 2023-04-24 21:57
**/
public class UserCookie {
/**
* 管理后台账号
* @param map
*/
public static void admin(Map<String, Object> map,Integer userId) {
// 存储权限api
/*List<Permission> list = permissionService.findPermissionListById(one.getId(),0,0);
StringBuilder sb = new StringBuilder(",");
list.forEach(i -> {
sb.append(i.getCode()).append(",");
});
map.put("apis",sb); // api权限*/
map.put("userType",1); // 设置为管理后台账号
}
/**
* 用户的账号
* @param map
*/
public static void user(Map<String, Object> map,Integer userId) {
map.put("userType",2); // 设置为用户账号
}
}
RequestUtil类代码如下,之所以在请求头中加上EXCEPTION_LOG_TOKEN变量,是为了区分请求的是否是异常日志执行调用的接口,方便后期扩展,请求头中key为EXCEPTION_LOG_TOKEN变量的值你可以记录其他信息,比如用户当时操作的ip和用户当时的操作时间,如果根据异常信息执行调用接口的话,记录的ip是本地ip,而非用户ip,记录的时间也是你执行异常信息时的时间,而非用户当时的操作时间
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* @program: newlookvin-parent
* @description 请求工具类
* @author: 影耀子(YingYew)
* @create: 2022-07-16 15:27
**/
public class RequestUtil {
/**
* 得到解析后的User-Agent
* @param request
* @return
*/
public static UserAgent getUserAgent(HttpServletRequest request) {
String uaStr = request.getHeader("User-Agent");
UserAgent ua = UserAgentUtil.parse(uaStr);
/*ua.getBrowser().toString();//Chrome 浏览器
ua.getVersion();//14.0.835.163
ua.getEngine().toString();//Webkit
ua.getEngineVersion();//535.1
ua.getOs().toString();//Windows 7
ua.getPlatform().toString();//Windows
*/
return ua;
}
// 异常日志重试接口时请求头的key
public static String EXCEPTION_LOG_TOKEN = "exceptionLogTOKEN";
/**
* 判断是否是从异常日志中重新执行的
* @param request
* @return
*/
public static Boolean isExceptionLogSource(HttpServletRequest request){
if (request != null && StringUtils.isNotBlank(request.getHeader(EXCEPTION_LOG_TOKEN))){
return true;
}
return false;
}
/**
* 异常日志接口重试中获取想要的数据
* @param request
* @return
*/
/*public static Object getExceptionLogSource(HttpServletRequest request,String key){
if (request != null && StringUtils.isNotBlank(request.getHeader(EXCEPTION_LOG_TOKEN))){
Claims claims = JwtUtil.parseJWT(request.getHeader(EXCEPTION_LOG_TOKEN));
return claims.get(key);
}
return null;
}*/
}
然后在ResolveExceptionUtil.exceptionQuery()方法中加上下图标记出来的代码,这是为了防止根据异常日志信息重新执行接口时重复记录异常日志信息。
本文章结束