目标:
最近公司有需求需要记录公司活动的修改日志,我就在想能不能做一个通用的日志记录
本篇代码较多, 主要是觉得光说不练假把式, 且为了同学可以拿走自己跑起来
-
目标
- 需要有日志记录操作的点支持自定义
- 需要记录的内容(字段)支持自定义
-
计划
类图(聚合关系)



使用到的工具类
可获取容器的实例的工具类
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @author yang
* @program
* @description 容器工具
* @create 2021/06/08 14:01
*/
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
//获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通过name获取 Bean.
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
//通过class获取Bean.
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
//通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
参数处理工具类
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.CodeSignature;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author yang
* @program
* @description 通用工具方法
* @create 2021/06/09 07:28
*/
@Slf4j
public class PointUtil {
/**
* 通过参数和标签获取storeIds
* @param point
* @param param
* @return
*/
public static List<Long> getIds(ProceedingJoinPoint point, String param){
Map<String, Object> map = getNameAndValue(point);
String[] parmas = param.split("\\.");
//参数本身为id
if(parmas.length==1){
Object o = map.get(parmas[0]);
if(o instanceof Long){
return Arrays.asList((Long)o);
}else if(o instanceof List){
return (List<Long>) o;
}else{
return new ArrayList<>();
}
//参数某个字段为id
}else{
Object o = map.get(parmas[0]);
String parmaName=parmas[1];
if(o instanceof List){
List<Object> list = (List) o;
return list.stream().map(l -> {
Long id = getIdByObject(l, parmaName);
return id;
}).collect(Collectors.toList());
}else{
return Arrays.asList(getIdByObject(o,parmaName));
}
}
}
/**
* 通过反射获取id
* @param obj
* @param paramsName;
* @return
*/
public static Long getIdByObject(Object obj,String paramsName) {
try {
Class<?> aClass = obj.getClass();
String methodName = "get"+paramsName.substring(0, 1).toUpperCase() + paramsName.substring(1);
Method method = aClass.getMethod(methodName);
return (Long) method.invoke(obj);
}catch (Exception e){
log.error("无法通过反射获得参数",e);
// 这个异常就是自己定义的运行时异常, 可以自定义
throw new RrException("无法通过反射获得参数:"+paramsName);
}
}
/**
* 获取参数Map集合
* @param joinPoint
* @return
*/
public static Map<String, Object> getNameAndValue(ProceedingJoinPoint joinPoint) {
Map<String, Object> param = new HashMap<>();
Object[] paramValues = joinPoint.getArgs();
String[] paramNames = ((CodeSignature)joinPoint.getSignature()).getParameterNames();
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return param;
}
}
获取两个对象不同的属性
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.Timestamp;
import java.util.*;
/**
* @author dongwang
* @version V1.0
* @since 2019-08-24 10:11
*/
public class ClassCompareUtils {
/**
* 比较两个实体属性值,返回一个boolean,true则表时两个对象中的属性值无差异
* @param oldObject 进行属性比较的对象1
* @param newObject 进行属性比较的对象2
* @return 属性差异比较结果boolean
*/
public static boolean compareObject(Object oldObject, Object newObject) {
Map<String, Map<String,Object>> resultMap=compareFields(oldObject,newObject);
if(resultMap.size()>0) {
return true;
}else {
return false;
}
}
/**
* 比较两个实体属性值,返回一个map以有差异的属性名为key,value为一个Map分别存oldObject,newObject此属性名的值
* @param oldObject 进行属性比较的对象1
* @param newObject 进行属性比较的对象2
* @return 属性差异比较结果map
*/
@SuppressWarnings("rawtypes")
public static Map<String, Map<String,Object>> compareFields(Object oldObject, Object newObject) {
Map<String, Map<String, Object>> map = null;
try{
/**
* 只有两个对象都是同一类型的才有可比性
*/
if (oldObject.getClass() == newObject.getClass()) {
map = new HashMap<String, Map<String,Object>>();
Class clazz = oldObject.getClass();
//获取object的所有属性
PropertyDescriptor[] pds = Introspector.getBeanInfo(clazz,Object.class).getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
//遍历获取属性名
String name = pd.getName();
//获取属性的get方法
Method readMethod = pd.getReadMethod();
// 在oldObject上调用get方法等同于获得oldObject的属性值
Object oldValue = readMethod.invoke(oldObject);
// 在newObject上调用get方法等同于获得newObject的属性值
Object newValue = readMethod.invoke(newObject);
if(oldValue instanceof List && newValue instanceof List){
Object[] oldArray = null;
Object[] newArrays = null;
if (oldValue != null && ((List) oldValue).size() > 0) {
if (((List) oldValue).get(0) instanceof Long) {
oldArray = ((List) oldValue).toArray(new Long[((List) oldValue).size()]);
}
if (((List) oldValue).get(0) instanceof String) {
oldArray = ((List) oldValue).toArray(new String[((List) oldValue).size()]);
}
}
if (newValue != null && ((List) newValue).size() > 0) {
if (((List) newValue).get(0) instanceof Long) {
newArrays = ((List) newValue).toArray(new Long[((List) newValue).size()]);
}
if (((List) newValue).get(0) instanceof String) {
newArrays = ((List) newValue).toArray(new String[((List) newValue).size()]);
}
}
if (!(oldArray == null && newArrays == null) && !Arrays.deepEquals(oldArray, newArrays)) {
Map<String,Object> valueMap = new HashMap<String,Object>();
valueMap.put("oldValue",oldValue);
valueMap.put("newValue",newValue);
map.put(name, valueMap);
}
}
if(oldValue instanceof Timestamp){
oldValue = new Date(((Timestamp) oldValue).getTime());
}
if(newValue instanceof Timestamp){
newValue = new Date(((Timestamp) newValue).getTime());
}
if(oldValue == null && newValue == null){
continue;
}else if(oldValue == null && newValue != null){
Map<String,Object> valueMap = new HashMap<String,Object>();
valueMap.put("oldValue",oldValue);
valueMap.put("newValue",newValue);
map.put(name, valueMap);
continue;
}
//比较这两个值是否相等,不等就可以放入map了
if (!oldValue.equals(newValue)) {
Map<String,Object> valueMap = new HashMap<String,Object>();
valueMap.put("oldValue",oldValue);
valueMap.put("newValue",newValue);
map.put(name, valueMap);
}
}
}
}catch(Exception e){
e.printStackTrace();
}
return map;
}
}
实现代码:
注解
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UpdateActivityTag {
/**
* 主键id所对应的参数名称
* 如果参数本身为 id 或者 id List 则直接传参数名
* 如果参数某个字段为 id 则传 参数名.字段名
* @return
*/
String idsParmas() default "";
/**
* 日志类型,可根据业务需求自定义
* @return
*/
int logType() default 0;
/**
* 服务名
*
* @return
*/
String serviceName() default "";
/**
* 需要校验保存的服务名 如果和unSaveField 同时存在 以需要的为准
*
* @return
*/
String[] needSaveField() default {};
/**
* 不需要校验保存的服务名
*
* @return
*/
String[] unSaveField() default {};
}
注解中的属性都是要在切面中使用到的
顶层抽象
import com.shuguolili.web.common.aspect.UpdateActivityTag;
import com.shuguolili.web.utils.ClassCompareUtils;
import org.springframework.scheduling.annotation.Async;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* @author yang
* @program
* @description 查询数据的顶层接口
* @create 2021/06/08 14:20
*/
public abstract class BaseQueryData {
public abstract Object QueryData(Long id);
public abstract Object QueryData(List<Long> ids);
@Async
public abstract void saveCompareLog(Object oldData, Object newData, Long recordId, String operName, Long operId, UpdateActivityTag tag);
public Map<String,Map<String, Object>> getUpdateLogDTO(Object oldData, Object newData, UpdateActivityTag tag) {
Map<String,Map<String, Object>> result = new HashMap<>();
Map<String, Map<String, Object>> maps = ClassCompareUtils.compareFields(oldData, newData);
if (maps.size() > 0) {
Iterator<String> keys = maps.keySet().iterator();
Map<String, Object> beforeMap = new HashMap<>();
Map<String, Object> afterMap = new HashMap<>();
while (keys.hasNext()) {
String key = keys.next();
if (checkNeedSave(key,tag)) {
Map<String, Object> values = maps.get(key);
String oldValue = getValue(values.get("oldValue"));
String newValue = getValue(values.get("newValue"));
beforeMap.put(key,oldValue);
afterMap.put(key,newValue);
}
}
if(afterMap.size() > 0) {
result.put("after",afterMap);
}
if(beforeMap.size() > 0 ) {
result.put("before",beforeMap);
}
}
return result;
}
private boolean checkNeedSave(String key,UpdateActivityTag tag) {
//如果是所有的字段都有保存,直接返回true
if(tag.allSave()){
return true;
}
if(tag.needSaveField() != null && tag.needSaveField().length > 0){
List<String> fields = Arrays.asList(tag.needSaveField());
return fields.contains(key);
}
if(tag.unSaveField() != null && tag.unSaveField().length > 0){
List<String> fields = Arrays.asList(tag.unSaveField());
return !fields.contains(key);
}
return false;
}
private String getValue(Object object) {
String value = "";
if (object != null) {
if (object instanceof Boolean) {
Boolean flag = (Boolean) object;
value = flag ? "是" : "否";
} else {
value = object.toString();
}
}
return value;
}
}
具体实现(其一)
只用一个来举例,如果有多个日志需要保存, 新增实现即可
import com.shuguolili.api.model.dto.UpdateLogDTO;
import com.shuguolili.api.service.read.IActivityInfoReadService;
import com.shuguolili.api.service.write.IUpdateLogWriteService;
import com.shuguolili.utils.JsonUtils;
import com.shuguolili.web.common.aspect.UpdateActivityTag;
import com.shuguolili.web.service.update.BaseQueryData;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* @author yang
* @program
* @description 活动的查询接口
* @create 2021/06/08 14:21
*/
@Service("activityInfoDataImpl")
public class ActivityInfoDataImpl extends BaseQueryData {
//实现类就是业务(保存日志)代码
@Reference(version = "1.0.0", group = "${dubbo.service.group}", check = false)
private IActivityInfoReadService activityInfoReadService;
@Reference(version = "1.0.0", group = "${dubbo.service.group}", check = false)
private IUpdateLogWriteService updateLogWriteService;
@Override
public Object QueryData(Long id) {
return activityInfoReadService.findDetailById(id);
}
@Override
public Object QueryData(List<Long> ids) {
return activityInfoReadService.listByIds(ids.toArray(new Long[0]));
}
@Override
public void saveCompareLog(Object oldData, Object newData, Long recordId, String operName, Long operId, UpdateActivityTag tag) {
if(oldData == null || newData == null){
return;
}
//根据数据中的自动的注解判断注解是否需要记录修改日志
Class<?> aClass = oldData.getClass();
if(!aClass.equals(newData.getClass())){
return;
}
//old 和 new 同种对象
UpdateLogDTO dto = new UpdateLogDTO();
//被修改的id
dto.setRecordId(recordId);
//日志类型,通过注解的tag.logType 传递
dto.setDataType(tag.logType());
//操作人id
dto.setOperatorId(operId);
//操作人姓名
dto.setOperatorName(operName);
//通过getUpdateLogDTO 获得被修改字段的修后值
Map<String, Map<String, Object>> updateLogDTO = getUpdateLogDTO(oldData, newData, tag);
Map<String, Object> before = updateLogDTO.get("before");
Map<String, Object> after = updateLogDTO.get("after");
// 将数据转换为json 这个工具比较常见就没有贴代码
if(before!= null && before.size()>0){
dto.setBeforData(JsonUtils.toJsonString(before));
}
if(after!= null && after.size()>0){
dto.setAfterData(JsonUtils.toJsonString(after));
}
//保存
if(dto.getAfterData() != null || dto.getBeforData() != null){
updateLogWriteService.save(dto);
}
}
}
切面
//定义日志类型的枚举
import com.shuguolili.api.model.enums.UpdateLogTypeEnum;
//这里有引用两个工具类
import com.shuguolili.web.common.util.PointUtil;
import com.shuguolili.web.common.util.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
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.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.List;
/**
* @author yang
* @program
* @description 修改日志
* @create 2021/06/08 11:11
*/
@Aspect
@Component
@Slf4j
public class ActivityInfoUpdateLongAspect {
@Pointcut("@annotation(com.shuguolili.web.common.aspect.UpdateActivityTag)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Parameter[] parameters = method.getParameters();
UpdateActivityTag tag = method.getAnnotation(UpdateActivityTag.class);
//通过注解 定位 参数 和 具体处理的实现类别名
//根据别名找到spring注册过得的服务bean实例,bean实例通过参数查询数据
List<Long> ids = PointUtil.getIds(point,tag.idsParmas());
BaseQueryData query = (BaseQueryData)SpringUtil.getBean(tag.serviceName());
//变更前
Object oldData = query.QueryData(ids.get(0));
//执行方法
Object result = point.proceed();
//变更后
Object newData = query.QueryData(ids.get(0));
//异步保存,通过自己的实现的服务是将修改前和修改后的数据保存到数据库就OK了
query.saveCompareLog(oldData,newData,ids.get(0), CurrentAdminUtil.getCurrentName(),CurrentAdminUtil.getCurrentId(),tag);
return result;
}
}
- 根据注解中提供的要素:
- 参数id/ids
- 需要/不需要校验保存的字段,
- spring注册的实例名,在切面中通过工具类获取服务,根据id分别查询修改后和修改前的数据, 调服务进行校验并保存日志记录
- 日志类型
- 是否所有字段都需要校验保存
总结
- 通过切面编程将需要有日志记录的地方统一加上日志记录(侵入性低)
- 通过注解达到不同的数据校验保存不同的字段变化(灵活性高)
- 通过抽象规范顶层规则,便于扩展(扩展性高)
如果对你有帮助希望点个赞, 鼓励下
另:如果考虑用到自己项目,有需要可以评论, 思想1+1 > 2 .
2.0版
- 在注解中添加新的属性serverClass,支持通过类名来指定服务的类名,方便功能扩展,降低了同事的学习成本
- 将通过@Async实现的异步替换为CompletableFuture.runAsync的方式实现,为抽取为starter 做铺垫
- 提供默认的class实现,并对异常信息处理,达到无侵入性,用户没有指定,不会影响原功能
/**
* 服务类
*
* @return
*/
Class serviceClass() default DefaultUpdateImpl.class;
切面around
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
UpdateLogTag tag = method.getAnnotation(UpdateLogTag.class);
List<Long> ids = PointUtil.getIds(point,tag.idsParmas());
BaseQueryData query = null;
try {
if (StringUtils.isNotBlank(tag.serviceName())) {
query = (BaseQueryData) SpringUtil.getBean(tag.serviceName());
} else {
query = (BaseQueryData) SpringUtil.getBean(tag.serviceClass());
}
} catch (Exception e) {
log.error("获取修改日志处理类失败,{}", ExceptionUtils.getStackTrace(e));
}
//变更前
Object oldData =null;
if(query != null) {
oldData = query.QueryData(ids.get(0));
}
//执行方法
Object result = point.proceed();
//变更后
if(query != null) {
Object newData = query.QueryData(ids.get(0));
//异步保存
Object finalOldData = oldData;
String currentName = CurrentAdminUtil.getCurrentName();
Long currentId = CurrentAdminUtil.getCurrentId();
BaseQueryData finalQuery = query;
CompletableFuture.runAsync(() -> finalQuery.saveCompareLog(finalOldData,newData,ids.get(0), currentName,currentId,tag));
}
return result;
}
后续有时间和机会,会将改功能抽取为stater ,提供通用的日志插件

本文介绍了一种通过切面编程和自定义注解来实现日志记录的方法,旨在提供一种灵活、低侵入性的日志管理系统。文章详细阐述了如何定义注解、切面、工具类以及相关实现,包括参数处理和对象差异比较。通过这种方式,可以自定义记录日志的字段,支持多种日志类型,并能适应不同服务的扩展需求。
655

被折叠的 条评论
为什么被折叠?



