最近有小伙伴问我,我们管理端的后端日志是如果实现的,是每个接口都需要加上一段保存日志的代码吗?
这样当然是可以实现的了,但是相比稍微有点开发经验的同学们,肯定此刻脑子已浮现出“AOP”了,废话不多扯了,直接上代码了。
丐版
实现效果如下,保存操作时的接口入参,实现简单,但是日志可读性不高。
public enum LogTypeEnum implements IBaseEnum<Integer> {
ADD(1, "新增数据"),
UPDATE (2, "修改数据"),
DELETE (3, "删除数据"),
UNKNOWN(-1, "未知"),
;
@Getter
private Integer value;
@Getter
private String label;
@Getter
private Integer type;
LogTypeEnum(Integer value, String label) {
this.value = value;
this.label = label;
}
}
@Aspect
@Component
@Slf4j
public class OperLogAspect {
@Resource
private SysLogService sysLogService;
/**
* 设置操作日志切入点 记录操作日志 在注解的位置切入代码
*/
@Pointcut("@annotation(com.oneam.cnps.annotation.OperLog)")
public void operLogPoinCut() {
}
/**
* 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行,
* 如果连接点抛出异常,则不会执行
*/
@AfterReturning(value = "operLogPoinCut()", returning = "keys")
public void saveOperLog(JoinPoint joinPoint, Object keys) {
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes
.resolveReference(RequestAttributes.REFERENCE_REQUEST);
SysLog sysLog = new SysLog();
try {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
OperLog operLog = method.getAnnotation(OperLog.class);
String operDesc = "";
LogTypeEnum logType = LogTypeEnum.UNKNOWN;
if (sysLog != null) {
logType = operLog.operType();
operDesc = operLog.operDesc();
}
// 获取方法入参
Object[] args = joinPoint.getArgs();
// 将参数所在的数组转换成json
String params = "";
if (args.length > 0) {
params = JSON.toJSONString(args[0]);
operDesc = operDesc + ",参数:" + params;
}
sysLog.setOperDesc(operDesc);
// 可以从请求数据里获取
String username = "";
sysLog.setOperUser(username);
sysLogService.saveLog(request, username, logType, operDesc);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
@OperLog(operType = LogTypeEnum.ADD, operDesc = "新增民俗文化数据")
升级版
日志的目的是为了方便查看数据修改记录,肯定是需要比较数据修改前后的数据项。
下面实现的前提是,工程已集成了mybaits-plus。
public interface ContentParser {
/**
* 获取信息返回查询出的对象
*
* @param joinPoint 查询条件的参数
* @param enableModifyLog 注解
* @return 获得的结果
*/
Object getOldResult(JoinPoint joinPoint, EnableModifyLog enableModifyLog);
/**
* 获取信息返回查询出的对象
*
* @param joinPoint 查询条件的参数
* @param enableModifyLog 注解
* @return 获得的结果
*/
Object getNewResult(JoinPoint joinPoint, EnableModifyLog enableModifyLog);
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
public @interface DataName {
/**
* 字段对应的中文注释
*/
String name() default "";
}
@Component
public class DefaultContentParse implements ContentParser {
@Override
public Object getOldResult(JoinPoint joinPoint, EnableModifyLog enableModifyLog) {
Object info = joinPoint.getArgs()[0];
Long id = (Long) ReflectionUtils.getFieldValue(info, "id");
Assert.notNull(id,"未解析到id值,请检查前台传递参数是否正确");
Class idType=enableModifyLog.idType();
if(idType.isInstance(id)){
Class cls=enableModifyLog.serviceClass();
IService service = (IService) SpringUtil.getBean(cls);
Object result=service.getById(id);
return result;
}else {
throw new RuntimeException("请核实id type");
}
}
@Override
public Object getNewResult(JoinPoint joinPoint, EnableModifyLog enableModifyLog) {
return getOldResult(joinPoint,enableModifyLog);
}
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface EnableModifyLog {
/**
* @return 操作的类型 可以直接调用ModifyName 不传时根据METHOD自动确定
*/
LogTypeEnum modifyType() default LogTypeEnum.UNKNOWN;
/**
* @return 获取编辑信息的解析类,目前为使用id获取,复杂的解析需要自己实现,默认不填写
* 则使用默认解析类
*/
Class<? extends ContentParser> parseClass() default DefaultContentParse.class;
/**
* @return 查询数据库所调用的class文件
*/
Class<? extends IService> serviceClass() default IService.class;
/**
* @return 是否需要默认的改动比较
*/
boolean needDefaultCompare() default false;
/**
* @return id的类型
*/
Class<?> idType() default Long.class;
/**
* @return 操作的描述
*/
String operDesc() default "";
}
/**
* 反射工具类.
* 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
*/
@SuppressWarnings("rawtypes")
public class ReflectionUtils {
private static final String SETTER_PREFIX = "set";
private static final String GETTER_PREFIX = "get";
private static final String CGLIB_CLASS_SEPARATOR = "$$";
private static Logger logger = LoggerFactory.getLogger(ReflectionUtils.class);
/**
* 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
* @param obj 读取的对象
* @param fieldName 读取的列
* @return 属性值
*/
public static Object getFieldValue(final Object obj, final String fieldName) {
Field field = getAccessibleField(obj, fieldName);
if (field == null) {
throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + obj + "]");
}
Object result = null;
try {
result = field.get(obj);
} catch (IllegalAccessException e) {
logger.error("不可能抛出的异常{}", e.getMessage());
}
return result;
}
/**
* 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问.
* 如向上转型到Object仍无法找到, 返回null.
* @param obj 查找的对象
* @param fieldName 列名
* @return 列
*/
public static Field getAccessibleField(final Object obj, final String fieldName) {
for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
try {
Field field = superClass.getDeclaredField(fieldName);
makeAccessible(field);
return field;
} catch (NoSuchFieldException e) {//NOSONAR
// Field不在当前类定义,继续向上转型
continue;// new add
}
}
return null;
}
/**
* 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
* @param field 列
*/
public static void makeAccessible(Field field) {
if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) || Modifier
.isFinal(field.getModifiers())) && !field.isAccessible()) {
field.setAccessible(true);
}
}
/**
* 获取两个对象同名属性内容不相同的列表
* @param class1 old对象
* @param class2 new对象
* @return 区别列表
* @throws ClassNotFoundException 异常
* @throws IllegalAccessException 异常
*/
public static List<Map<String ,Object>> compareTwoClass(Object class1, Object class2) throws ClassNotFoundException, IllegalAccessException {
List<Map<String,Object>> list=new ArrayList<Map<String, Object>>();
//获取对象的class
Class<?> clazz1 = class1.getClass();
Class<?> clazz2 = class2.getClass();
//获取对象的属性列表
Field[] field1 = clazz1.getDeclaredFields();
Field[] field2 = clazz2.getDeclaredFields();
StringBuilder sb=new StringBuilder();
//遍历属性列表field1
for(int i=0;i<field1.length;i++) {
//遍历属性列表field2
for (int j = 0; j < field2.length; j++) {
//如果field1[i]属性名与field2[j]属性名内容相同
if (field1[i].getName().equals(field2[j].getName())) {
if (field1[i].getName().equals(field2[j].getName())) {
field1[i].setAccessible(true);
field2[j].setAccessible(true);
//如果field1[i]属性值与field2[j]属性值内容不相同
if (!compareTwo(field1[i].get(class1), field2[j].get(class2))) {
Map<String, Object> map2 = new HashMap<String, Object>();
DataName name=field1[i].getAnnotation(DataName.class);
String fieldName="";
if(name!=null){
fieldName=name.name();
}else {
fieldName=field1[i].getName();
}
map2.put("name", fieldName);
map2.put("old", field1[i].get(class1));
map2.put("new", field2[j].get(class2));
list.add(map2);
}
break;
}
}
}
}
return list;
}
/**
* 对比两个数据是否内容相同
*
* @param object1 比较对象1
* @param object2 比较对象2
* @return boolean类型
*/
public static boolean compareTwo(Object object1,Object object2){
if(object1==null&&object2==null){
return true;
}
if(object1==null&&object2!=null){
return false;
}
if(object1.equals(object2)){
return true;
}
return false;
}
}
@Slf4j
public class ObjectCompareUtil {
public static String defaultDealUpdate(Object newObject, Map<String, Object> oldMap) {
try {
AtomicReference<Boolean> isUpdate = new AtomicReference<>(false);
Map<String, Object> newMap = (Map<String, Object>) objectToMap(newObject);
Long id = (Long) newMap.get("id");
StringBuilder str = new StringBuilder();
str.append("修改了ID为【").append(id).append("】的数据;\n");
Object finalNewObject = newObject;
oldMap.forEach((k, v) -> {
Object newResult = newMap.get(k);
if ((v==null && newResult !=null) || (v != null && !v.equals(newResult))) {
Field field = ReflectionUtils.getAccessibleField(finalNewObject, k);
if(!field.getName().equals("updateTime")){
isUpdate.set(true);
DataName dataName = field.getAnnotation(DataName.class);
if (dataName != null) {
str.append("【").append(dataName.name()).append("】从【")
.append(v).append("】改为了【").append(newResult).append("】;\n");
} else {
str.append("【").append(field.getName()).append("】从【")
.append(v).append("】改为了【").append(newResult).append("】;\n");
}
}
}
});
if (!isUpdate.get()) {
return null;
}
return str.toString();
} catch (Exception e) {
log.error("比较异常", e);
throw new RuntimeException("比较异常", e);
}
}
public static String defaultDealUpdate(Object newObject, Object oldObject) {
try {
AtomicReference<Boolean> isUpdate = new AtomicReference<>(false);
Map<String, Object> newMap = (Map<String, Object>) objectToMap(newObject);
Map<String, Object> oldMap = (Map<String, Object>) objectToMap(oldObject);
Long id = (Long) newMap.get("id");
StringBuilder str = new StringBuilder();
str.append("修改了ID为【").append(id).append("】的数据;\n");
Object finalNewObject = newObject;
oldMap.forEach((k, v) -> {
Object newResult = newMap.get(k);
if (v != null && !v.equals(newResult)) {
Field field = ReflectionUtils.getAccessibleField(finalNewObject, k);
if(!field.getName().equals("updateTime")){
isUpdate.set(true);
DataName dataName = field.getAnnotation(DataName.class);
if (dataName != null) {
str.append("【").append(dataName.name()).append("】从【")
.append(v).append("】改为了【").append(newResult).append("】;\n");
} else {
str.append("【").append(field.getName()).append("】从【")
.append(v).append("】改为了【").append(newResult).append("】;\n");
}
}
}
});
if (!isUpdate.get()) {
return null;
}
return str.toString();
} catch (Exception e) {
log.error("比较异常", e);
throw new RuntimeException("比较异常", e);
}
}
public static Map<?, ?> objectToMap(Object obj) {
if (obj == null) {
return null;
}
ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//mapper.addMixIn(Object.class, IgnoreHibernatePropertiesInJackson.class);
Map<?, ?> mappedObject = mapper.convertValue(obj, Map.class);
return mappedObject;
}
}
@Aspect
@Component
public class ModifyAspect {
private final static Logger logger = LoggerFactory.getLogger(ModifyAspect.class);
@Resource
private SysLogService sysLogService;
@Around("@annotation(enableModifyLog)")
public Object around(ProceedingJoinPoint joinPoint, EnableModifyLog enableModifyLog) throws Throwable {
Map<String, Object> oldMap = new HashMap<>();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
if (LogTypeEnum.UPDATE.equals(enableModifyLog.modifyType())) {
try {
ContentParser contentParser = (ContentParser) SpringUtil.getBean(enableModifyLog.parseClass());
Object oldObject = contentParser.getOldResult(joinPoint, enableModifyLog);
if (enableModifyLog.needDefaultCompare()) {
oldMap = (Map<String, Object>) ObjectCompareUtil.objectToMap(oldObject);
}
} catch (Exception e) {
logger.error("service加载失败:", e);
}
}
Object object = joinPoint.proceed();
Object newObject = null;
if (LogTypeEnum.UPDATE.equals(enableModifyLog.modifyType())) {
ContentParser contentParser;
try {
contentParser = (ContentParser) SpringUtil.getBean(enableModifyLog.parseClass());
newObject = contentParser.getNewResult(joinPoint, enableModifyLog);
} catch (Exception e) {
logger.error("service加载失败:", e);
}
String operDesc = enableModifyLog.operDesc();
String updateContent = ObjectCompareUtil.defaultDealUpdate(newObject, oldMap);
if(StringUtils.isNotBlank(updateContent)){
logger.info("修改内容为:{}", updateContent);
sysLogService.saveLog(request, AuthContextUtil.getUsername(), LogTypeEnum.UPDATE, operDesc + ":" +updateContent);
}
}
return object;
}
}