关联文章:《AOP编程:理解Spring框架中的切面、通知与切入点》和《aop应用-1 使用注解,返回统一数据格式》
接着,讲统一处理日志。 在平时中,对于一些接口,要记录参数和返回值,这时候,就适合用添加注解,统一去处理。
数据库表:
要记录参数和返回值,这个记录到数据库里面。
参数日志表:
CREATE TABLE `common_api_log` (
`log_id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`staff_id` BIGINT(20) DEFAULT NULL COMMENT '操作人ID',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`log_flag` VARCHAR(12) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '执行状态',
`log_key` VARCHAR(600) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '日志key',
`log_type` VARCHAR(150) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '日志类型',
`can_repeat` VARCHAR(12) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '是否可重复',
`request_addr` VARCHAR(768) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求地址',
`handle_method` VARCHAR(1500) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '处理方法',
`log_class` VARCHAR(150) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '日志类',
`parent_log_id` BIGINT(15) DEFAULT NULL COMMENT '父级logId',
`request_type` VARCHAR(600) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求类型',
`request_count` INT(11) NOT NULL COMMENT '请求次数',
`log_desc` TEXT COLLATE utf8mb4_bin COMMENT '请求的参数',
`request_id` VARCHAR(108) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求id',
`request_ip` VARCHAR(192) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求ip',
`request_method` VARCHAR(1500) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求的方法',
PRIMARY KEY (`log_id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT 'aip日志表';
请求结果日志表:
CREATE TABLE `common_api_log_result` (
`detail_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '详情标识',
`log_id` BIGINT(20) DEFAULT NULL COMMENT '日志标识',
`log_result` LONGTEXT COLLATE utf8_bin COMMENT '响应结果',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`request_time` DATETIME DEFAULT NULL COMMENT '请求时间',
`log_flag` VARCHAR(50) COLLATE utf8_bin DEFAULT NULL COMMENT '执行状态',
`response_time` DATETIME DEFAULT NULL COMMENT '响应时间',
PRIMARY KEY (`detail_id`) USING BTREE,
KEY `idx_form_log_id` (`log_id`) USING BTREE
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC COMMENT='api日志操作结果';
代码:
entity
CommonApiLog
@Data
public class CommonApiLog {
private Long logId;
private Long staffId;
private String staffName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String logFlag;
private String logKey;
private String logType;
private String canRepeat;
private String requestAddr;
private String requestMethod;
private String logClass;
private Long parentLogId;
private String requestType;
private Integer requestCount;
private String logDesc;
private List<CommonApiLogResult> apiLogDetails;
}
CommonApiLogResult
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@Data
public class CommonApiLogResult {
private Long detailId;
private Long logId;
private String logResult;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date requestTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date responseTime;
private String logFlag;
}
utils:
SpringUtils
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
public SpringUtils() {
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtils.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}
StringUtil
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class StringUtil {
public static String emptyStringSort(String... args) {
for (String arg : args) {
if (StringUtils.isNotEmpty(arg)) {
return arg;
}
}
return null;
}
public static String nullStringSort(String... args) {
for (String arg : args) {
if (arg != null) {
return arg;
}
}
return null;
}
public static boolean isArray(Object object) {
Class cls = object.getClass();
if (cls.isArray()) {
return true;
}
return false;
}
public static boolean isObject(Object object) {
if (isArray(object)) {
return true;
}
if (isList(object)) {
return true;
}
if (isMap(object)) {
return true;
}
return false;
}
public static boolean isList(Object object) {
if (object instanceof List) {
return true;
}
return false;
}
public static boolean isNull(String str) {
return str == null || str.equals("") || str.toLowerCase().equals("null");
}
public static boolean isMap(Object object) {
if (object instanceof Map) {
return true;
}
return false;
}
public static Object arrayToString(Object object) {
if (object == null) {
return "";
}
if (isArray(object)) {
return StringUtils.join(Arrays.asList(object), ",");
}
if (isList(object)) {
return String.join(",", (List) object);
}
return object;
}
public static Object stringToObject(Object object) {
if (object == null) {
return null;
}
if (isObject(object)) {
return object;
}
return JSONObject.parse(String.valueOf(object));
}
public static Object objectToString(Object object) {
if (object == null) {
return null;
}
if (object instanceof String) {
return object;
}
return JSONObject.toJSONString(object, SerializerFeature.DisableCircularReferenceDetect);
}
public static Object stringToArray(Object object) {
if (object == null) {
return null;
}
if (isObject(object)) {
return object;
}
return Arrays.asList(String.valueOf(object).split(","));
}
public static String parseValue(Object value) {
if (value == null) {
return "";
}
return String.valueOf(value);
}
public static boolean contains(String source, String dest) {
if (source == null || dest == null)
return false;
if (Arrays.asList(source.split(",")).contains(dest)) {
return true;
}
return false;
}
public static String valueOf(Object value) {
if (value == null) {
return "";
}
return String.valueOf(value);
}
public static boolean isEmpty(Object value) {
if (value == null) {
return true;
}
return "".equals(value);
}
public static boolean isContains(String sValue, String cValue) {
if (StringUtils.isEmpty(sValue) || StringUtils.isEmpty(cValue)) {
return false;
}
List<String> cValues = Arrays.asList(cValue.split(","));
return cValues.contains(sValue);
}
public static String remove(String src, String dest) {
if (StringUtils.isEmpty(src))
return "";
List<String> srcList = Arrays.asList(src.split(","));
srcList.remove(dest);
return srcList.stream().collect(Collectors.joining(","));
}
}
aop:
注解:CommonLog
import java.lang.annotation.*;
/**
* 日志注解 只能作用于受spring管理的实现类或方法上
* 可以使用spel表达式取参数的值 例如: #param.id
*
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CommonLog {
String logKey() default "";
String logType() default "0";
String logMethod() default "";
String logClass() default "";
String requestType() default "s";
String canRepeat() default "T";
/**
* 加在方法的参数上 用于忽略该参数
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface Ignore {
}
}
LogAnnotationAdvisor
1,参数的获取:
在around 方法里面,pjp.proceed() 执行前,把参数,做一个记录。
2,请求结果的获取: pjp.proceed() 记录执行的结果
对于参数的解析,使用SpingEL
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.yan.web.spring.aop.log.entity.CommonApiLog;
import com.yan.web.spring.aop.log.entity.CommonApiLogResult;
import com.yan.web.spring.aop.log.util.SpringUtils;
import com.yan.web.spring.aop.log.util.StringUtil;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.apache.commons.lang.WordUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class LogAnnotationAdvisor implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(LogAnnotationAdvisor.class);
private static final ThreadLocal<LinkedList<Boolean>> FLAG = new ThreadLocal<>();
private static ThreadPoolExecutor pool;
private final ExpressionParser parser = new SpelExpressionParser();
@Pointcut("@annotation(com.yan.web.spring.aop.log.CommonLog)||" +
"@within(com.yan.web.spring.aop.log.CommonLog)")
public void log() {
}
public static String toJsonString(Object o) {
if (StringUtil.isEmpty(o)) {
return null;
}
if (o instanceof String) {
return (String) o;
}
try {
return JSON.toJSONString(o, SerializerFeature.DisableCircularReferenceDetect,
SerializerFeature.IgnoreNonFieldGetter, SerializerFeature.WriteClassName);
} catch (Throwable e) {
logger.error(e.getMessage(), e);
return o.toString();
}
}
public static void setFlag(Boolean flag) {
if (FLAG.get() == null) {
FLAG.set(new LinkedList<>());
}
FLAG.get().addLast(flag);
}
public static void removeFlag() {
LinkedList<Boolean> flags = FLAG.get();
if (flags != null) {
Boolean flag = flags.pollLast();
if (flag == null || CollectionUtils.isEmpty(flags)) {
FLAG.remove();
}
}
}
private Boolean getFlag() {
LinkedList<Boolean> flags = FLAG.get();
return flags == null ? null : flags.peekLast();
}
@Around(value = "log()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
logger.debug("-----do around log-----");
if (!Optional.ofNullable(getFlag()).orElse(Boolean.TRUE)) {
return pjp.proceed();
}
CommonApiLog apiLog = new CommonApiLog();
boolean addLog = true;
Object logResult = null;
try {
try {
buildRequestInfo(pjp, apiLog);
} catch (Exception e) {
logger.error(e.getMessage(), e);
addLog = false;
}
Object response = pjp.proceed();
apiLog.setLogFlag("1");
logResult = response;
return response;
} catch (Throwable t) {
apiLog.setLogFlag("0");
logResult = t;
throw t;
} finally {
if (addLog) {
Date now = new Date();
sendApiLog(apiLog, logResult, now);
}
}
}
private boolean canNotInvokeClass(Class<?> clazz) {
List<Class<?>> classes = Arrays.asList(ServletResponse.class, ServletRequest.class, HttpSession.class,
MultipartFile.class, InputStream.class, OutputStream.class);
for (Class<?> c : classes) {
if (c.isAssignableFrom(clazz)) {
return true;
}
}
return false;
}
// 获取和拼接请求参数
private void buildRequestInfo(ProceedingJoinPoint pjp, CommonApiLog apiLog) {
MethodSignature sign = (MethodSignature) pjp.getSignature();
Method method = sign.getMethod();
CommonLog commonLog = method.getAnnotation(CommonLog.class);
if (commonLog == null) {
commonLog = method.getDeclaringClass().getAnnotation(CommonLog.class);
}
logger.info("commonLog:{}", commonLog);
String[] parameterNames = sign.getParameterNames();
Class<?>[] parameterTypes = sign.getParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Object[] parameters = pjp.getArgs();
Map<String, Map<String, Object>> logRequest = new LinkedHashMap<>(parameters.length);
EvaluationContext context = new StandardEvaluationContext();
String canRepeat = null;
if (parameterNames != null && parameterNames.length > 0) {
for (int i = 0, n = parameterNames.length; i < n; i++) {
String parameterName = parameterNames[i];
Object parameter = parameters[i];
Class<?> parameterType = parameterTypes[i];
context.setVariable(parameterName, parameter);
if (canRepeat == null && canNotInvokeClass(parameterType)) {
canRepeat = "F";
}
String beanName = parameter == null ? null : getSpringBeanName(parameter.getClass());
if (parameter instanceof HttpSession || parameter instanceof HttpServletRequest
|| parameter instanceof HttpServletResponse || hasIgnoreAnnotation(parameterAnnotations[i])) {
logRequest.put(parameterName, new HashMap<>(Collections.singletonMap(parameterType.getName(),
StringUtils.isBlank(beanName) ? null : "Bean@" + beanName)));
} else if (parameter instanceof MultipartFile) {
logRequest.put(parameterName, new HashMap<>(Collections.singletonMap(parameterType.getName(),
((MultipartFile) parameter).getOriginalFilename())));
} else if (StringUtils.isNotBlank(beanName)) {
logRequest.put(parameterName,
new HashMap<>(Collections.singletonMap(parameterType.getName(), "Bean@" + beanName)));
} else {
logRequest.put(parameterName,
new HashMap<>(Collections.singletonMap(parameterType.getName(), parameter)));
}
}
}
apiLog.setCreateTime(new Date());
apiLog.setStaffId(1L); // 获取登录人的staffId
apiLog.setLogDesc(toJsonString(logRequest));
apiLog.setLogKey(getSpelValue(context, commonLog.logKey()));
apiLog.setLogType(getSpelValue(context, commonLog.logType()));
apiLog.setLogClass(getSpelValue(context, commonLog.logClass()));
String logMethod = getSpelValue(context, commonLog.logMethod());
// 获取注解的logMethod
if (StringUtils.isBlank(logMethod)) {
apiLog.setRequestMethod(pjp.getTarget().getClass().getName() + "#" + method.getName());
} else {
apiLog.setRequestMethod(pjp.getTarget().getClass().getName() + "#" + logMethod);
}
apiLog.setRequestCount(1);
apiLog.setRequestType(getSpelValue(context, commonLog.requestType()));
if (canRepeat != null) {
apiLog.setCanRepeat(canRepeat);
} else {
apiLog.setCanRepeat(getSpelValue(context, commonLog.canRepeat()));
}
}
private String getSpringBeanName(Class<?> clazz) {
String beanName = null;
if (clazz.isAnnotationPresent(Service.class)) {
beanName = clazz.getAnnotation(Service.class).value();
} else if (clazz.isAnnotationPresent(Component.class)) {
beanName = clazz.getAnnotation(Component.class).value();
}
if (StringUtils.isBlank(beanName)) {
try {
String simpleName = clazz.getSimpleName();
// WordUtils.uncapitalize 首字母转小写,比如 Abcd 变成 abcd
SpringUtils.getBean(WordUtils.uncapitalize(simpleName));
beanName = simpleName;
} catch (Throwable t) {
try {
SpringUtils.getBean(clazz);
beanName = clazz.getName();
} catch (Throwable tt) {
//ignore
}
}
}
return beanName;
}
private void sendApiLog(CommonApiLog apiLog, Object logResult, Date responseTime) {
pool.execute(() -> {
CommonApiLogResult apiLogDetail = new CommonApiLogResult();
apiLogDetail.setCreateTime(apiLog.getCreateTime());
apiLogDetail.setRequestTime(apiLog.getCreateTime());
apiLogDetail.setResponseTime(responseTime);
apiLogDetail.setLogFlag(apiLog.getLogFlag());
if (logResult != null) {
String logDesc;
if ("1".equals(apiLog.getLogFlag())) {
logDesc = toJsonString(logResult);
} else {
logDesc = getTraceInfo((Throwable) logResult);
}
if (!StringUtil.isNull(logDesc)) {
apiLogDetail.setLogResult(logDesc);
}
}
apiLog.setApiLogDetails(Collections.singletonList(apiLogDetail));
try {
// 保存日志到数据库
// addCommonApiLog(apiLog);
} catch (Exception e) {
logger.error("apiLog:{}", toJsonString(apiLog), e);
}
});
}
private boolean hasIgnoreAnnotation(Annotation[] annotations) {
boolean ignore = false;
if (annotations != null && annotations.length > 0) {
for (Annotation annotation : annotations) {
if (annotation instanceof CommonLog.Ignore) {
ignore = true;
break;
}
}
}
return ignore;
}
// 获取表达式的值
private String getSpelValue(EvaluationContext context, String template) {
if (StringUtils.isBlank(template)) {
return null;
}
if (!template.startsWith("#")) {
return template;
}
try {
Expression expression = parser.parseExpression(template);
return expression.getValue(context, String.class);
} catch (Exception e) {
logger.error(e.getMessage(), e);
return template;
}
}
public static String getTraceInfo(Throwable t) {
try (Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer)) {
t.printStackTrace(printWriter);
return writer.toString();
} catch (Exception e) {
return t.getMessage();
}
}
@After(value = "log()")
public void doAfter() {
logger.debug("-----do after log-----");
}
@Override
public void afterPropertiesSet() {
// Runtime.getRuntime().availableProcessors() 获取当前系统可用的逻辑处理器的数量
int size = Runtime.getRuntime().availableProcessors() + 1;
pool = new ThreadPoolExecutor(size, size * 2, 120L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
}
}
使用:
@WorkflowLog(logKey = "#request.contentId")
public void delEvent(EventWorkSheetRequest request) {}
@WorkflowLog(logKey = "#processData.process.id")
public ValidationErr saveProgress(ProcessData processData,
@WorkflowLog.Ignore IProcess iProcess) throws FormEngineException {
}
总结:
使用aop注解,统一记录请求参数和日志,便于查询日志。这个时间长了,数据库的日志表的数据会很多,要定期进行清理。