新乡河南网站建设如何提高网站在搜索引擎中的排名
这里写目录标题
- 目标:
- 类图(聚合关系)
- 使用到的工具类
- 可获取容器的实例的工具类
- 参数处理工具类
- 获取两个对象不同的属性
- 实现代码:
- 注解
- 顶层抽象
- 具体实现(其一)
- 切面
- 总结
- 2.0版
- 切面around
目标:
最近公司有需求需要记录公司活动的修改日志,我就在想能不能做一个通用的日志记录
本篇代码较多, 主要是觉得光说不练假把式, 且为了同学可以拿走自己跑起来
-
目标
- 需要有日志记录操作的点支持自定义
- 需要记录的内容(字段)支持自定义
-
计划
- 使用切面完成修改记录
- 切点由自定义的注解定位,切点的属性
- 参数 id params
- 需要记录的字段 needSaveField
- 日志类型
- 是否全部字段需要记录
- 不需要记录的字段 unSaveField1
- 获取修改数据的服务 serviceName2
类图(聚合关系)
使用到的工具类
可获取容器的实例的工具类
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;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {if(SpringUtil.applicationContext == null) {SpringUtil.applicationContext = applicationContext;}}//获取applicationContextpublic 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返回指定的Beanpublic 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("\\.");//参数本身为idif(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);@Asyncpublic 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) {//如果是所有的字段都有保存,直接返回trueif(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;@Overridepublic Object QueryData(Long id) {return activityInfoReadService.findDetailById(id);}@Overridepublic Object QueryData(List<Long> ids) {return activityInfoReadService.listByIds(ids.toArray(new Long[0]));}@Overridepublic 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();//被修改的iddto.setRecordId(recordId);//日志类型,通过注解的tag.logType 传递dto.setDataType(tag.logType());//操作人iddto.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 ,提供通用的日志插件
需要记录的字段和不需要记录的字段互斥,当两个都存在时以需要记录的字段为准 ↩︎
serviceName 是通过id params查询到别修改的数据的服务,且服务都是继承了顶层接口的实现类, 这样统一服务规则 ↩︎