前言
近期公司历史项目需要增加一个记录操作记录的功能,但由于项目已经是中后期了,无可避免的增加耦合度。
思路
看到原型后,内部也进行了相应的探讨。耦合度肯定是无可避免的,只能尽量避免!
具体实现的方案也有如下几种:
1、后端提供记录日志的统一接口,由前端在操作调用部分需要记录的模块中请求完成后再调用记录日志接口。×
2、后端定义拦截器,对相应的操作请求接口进行拦截记录,同时由前端在请求头定义自定义参数:操作模块,菜单等信息。×
3、全部交由后端进行处理,AOP自定义注解形式。√
4、使用第三方框架ObjectLogger。×
关于以上几种方案,弊端也很明显。
第一种,后端耦合度低但是全部交由前端了,由前端进行埋点,增加了前端的负担。同时由于是请求回调后进行记录,可能存在网络问题导致丢失记录的情况,所以被毙。
第二种,定义统一拦截器,后端耦合度低了前端也无需埋点,只需要改造一下请求头即可。但是前端也需要定义好各种操作以及菜单等信息,耦合度也无法降低。
第三种,采用。弊端就是无法精准的定位操作位置和方式,当然这也取决于你项目架构的方式。比如编辑和添加都是一个接口,AOP难以区分你是具体操作。
第四种,版本时间有限,由于没有使用过,没法保证在时间内完成,并且该框架是基于单条数据的日志记录。
涉及技术
Spring Boot 2.X、json-lib、fastjson、commons-lang3
详细说明
- 新建自定义注解@Log
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented //生成文档
public @interface Log {
/**
* 字段key
* @return
*/
String key() default "";
/**
* 操作模块
* @return
*/
String module() default "";
/**
* 操作类型
* @return
*/
OperationType operationType() default OperationType.OTHER;
/**
* 操作简述
* @return
*/
String describe() default "";
}
- 新建操作类型枚举
public enum OperationType {
/**
* 新增
*/
ADD,
/**
* 删除
*/
DEL,
/**
* 编辑
*/
EDIT,
/**
* 导出
*/
EXPORT,
/**
* 查询
*/
SELECT,
/**
* 登录
*/
LOGON,
/**
* 上下架
*/
POS,
/**
* 出入库
*/
WH,
/**
* 打印
*/
PRINT,
/**
* 审核
*/
REVIEW,
/**
* 其它
*/
OTHER;
public String getType() {
if (this.equals(ADD)) {
return "新增";
}
if (this.equals(EDIT)) {
return "编辑";
}
if (this.equals(DEL)) {
return "删除";
}
if (this.equals(EXPORT)) {
return "导出";
}
if (this.equals(SELECT)) {
return "查询";
}
if (this.equals(LOGON)) {
return " 登陆";
}
if (this.equals(POS)) {
return "上/下架";
}
if (this.equals(WH)) {
return "出/入库";
}
if (this.equals(PRINT)) {
return "打印";
}
if (this.equals(REVIEW)) {
return "审核";
}
if (this.equals(OTHER)) {
return "其它";
}
return null;
};
}
- 日志实体
@Entity
@Table(name="system_log")
@NamedQuery(name="SystemLog.findAll", query="SELECT s FROM SystemLog s")
public class SystemLog implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private String id;
/**
* 操作时间
*/
@Column(name="create_time")
private Date createTime;
/**
* 操作用户ID
*/
@Column(name="user_id")
private String userId;
/**
* 用户姓名
*/
@Column(name="user_name")
private String userName;
/**
* 用户电话
*/
@Column(name="user_tel")
private String userTel;
/**
* 操作模块*/
private String module;
/**操作类型*/
@Enumerated(EnumType.STRING)
@Column(name="operation_type")
private OperationType operationType;
/**
* 操作类型 名称
*/
@Column(name="operation_type_name")
private String operationTypeName;
/**
* 关键字名称
* */
@Column(name="key_name")
private String keyName;
/**
* 操作数据的关键字 内容
* */
@Column(name="key_values")
private String keyValues;
/**
*操作简述
*/
@Column(name="operation_describe")
private String operationDescribe;
/**
* 操作执行的方法
*/
private String method;
/**
* 方法参数
* */
@Lob
@Column(columnDefinition="text")
private String params;
/**
* 数据ID
*/
@Column(name="data_id")
private String dataId;
/**
* 操作ip地址
*/
@Column(name="ip_address")
private String ipAddress;
}
- 建立切面
/**
* 系统日志 切面
*
* @author liu
* @date 2020-08-19 17:04:04
*/
@Aspect
@Component
public class SystemLogAspects{
@Autowired
protected SystemLogRepository systemLogRepository;//Spring JPA增删改查接口
/**
* 定义切点 @Pointcut
* 在注解的位置切入代码
*/
@Pointcut("@annotation(com.xx.xxx.log.Log)")//自定义注解路径
public void logPoinCut() {
}
/**切面 配置通知,各注解作用
*@Before: 前置通知,在方法执行之前执行
*@After: 后置通知,在方法执行之后执行
*@AfterRunning: 返回通知,在方法返回结果之后执行,用这个注解参数会随方法改变,例新增一个实体,参数是一个id为null的,用这个注解后就会赋上真实的id
*@AfterThrowing: 异常通知,在方法抛出异常之后执行
*@Around: 环绕通知,围绕着方法执行
* */
@AfterReturning("logPoinCut()")
public void saveSysLog(JoinPoint joinPoint) {
try{
SystemLog sysLog = new SystemLog();
//从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
//获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
//获取请求的方法名
String methodName = method.getName();
//请求的参数
Object[] args = joinPoint.getArgs();
//操作模块
String module;
//key 关键字值
String keyValue;
//操作简述
String describe;
//操作类型的枚举
OperationType operationType;
//获取操作用户信息
SystemUser userInfo = getUserInfo(signature,args);
//记录操作人信息
sysLog.setUserId(userInfo.getId());
sysLog.setUserName(userInfo.getUserName());
sysLog.setUserTel(userInfo.getUserPhone());
Log log = method.getAnnotation(Log.class);
//获取被切参数名称及参数值
JSONObject json = getFieldsName(this.getClass(), className, methodName, args);
if (log != null) {
//操作模块
module = !"".equals(log.module()) ?log.module():"未知";
sysLog.setModule(module);
//操作类型
operationType= log.operationType()!=null?log.operationType():OperationType.OTHER;
sysLog.setOperationType(operationType);
sysLog.setOperationTypeName(operationType.getType());
//根据关键字获取关键字的value值
keyValue = parseKeyValues(log.key().trim(),json);
sysLog.setKeyName(log.key());
sysLog.setKeyValues(keyValue);
//简述
describe = !"".equals(log.describe()) ?log.describe():"无";
//简述拼接 【会员中心】修改【100020080016】用户信息
StringBuilder operationDescribe = new StringBuilder("【"+module+"】"+operationType.getType());
if(StringUtils.isNotEmpty(keyValue)){
operationDescribe.append("【").append(keyValue).append("】");
}
operationDescribe.append(describe);
sysLog.setOperationDescribe(operationDescribe.toString());
}
sysLog.setParams(json.toString());
sysLog.setId(UUIDUtils.getUUID());
sysLog.setCreateTime(new Date());
sysLog.setMethod(className + "." + methodName);
sysLog.setIpAddress(IPaddressUtils.getInnetIp());
//保存日志
systemLogRepository.save(sysLog);
}catch (Exception e){
logger.error("日志记录失败:记录操作日志发生异常错误!",e);
}
}
/**
* 根据字段名获取对应的值内容
* @param specifyKey 字段名
* @param json 参数json
* @return
*/
public static String parseKeyValues(String specifyKey, JSONObject json) {
String ret = "";
try{
//获取所有键
Iterator keys=json.keys();
while (keys.hasNext()) {
//迭代
String key = (String) keys.next();
//获取第一个键的值
String keyValue = json.get(key).toString();
//判断值是否为json格式
if (JsonUtils.isJsonObject(keyValue)){
//是json 继续迭代执行
String value = parseKeyValues(specifyKey, JSONObject.fromObject(json.get(key)));
//有内容说明匹配上了 可以退出
if(!StringUtils.isEmpty(value)){
ret = value;
break;
}
}
//非 json对象,对比键是否一致
if(key.equals(specifyKey)){
ret = keyValue;
break;
}
}
return ret;
}catch (Exception e){
logger.error("日志记录失败:参数查找发生异常",e);
}
return ret;
}
/**
* 根据token获取用户id
* @param signature
* @param args
* @return
*/
public SystemUser getUserInfo(MethodSignature signature,Object[] args){
SystemUser systemUser = new SystemUser();
try{
String token="";
//获取参数名称
String[] parameterNames = signature.getParameterNames();
//查找token所在的下标
int tokenIndex = ArrayUtils.indexOf(parameterNames, "token");
if (tokenIndex != -1) {
token = (String) args[tokenIndex];
}
//取到token
if(!StringUtils.isEmpty(token)){
//获取用户登陆信息
systemUser = getUserByToken(token);
}
}catch (Exception e){
logger.error("日志记录失败:根据token获取用户信息出现异常",e);
}
return systemUser;
}
/**
* 通过反射机制 获取被切参数名以及参数值
*
* @param cls
* @param clazzName
* @param methodName
* @param args
* @return
* @throws NotFoundException
*/
private JSONObject getFieldsName(Class cls, String clazzName, String methodName, Object[] args){
Map<String, Object> map = new HashMap<>(16);
try{
ClassPool pool = ClassPool.getDefault();
ClassClassPath classPath = new ClassClassPath(cls);
pool.insertClassPath(classPath);
CtClass cc = pool.get(clazzName);
CtMethod cm = cc.getDeclaredMethod(methodName);
MethodInfo methodInfo = cm.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
for (int i = 0; i < cm.getParameterTypes().length; i++) {
//paramNames即参数名
map.put(attr.variableName(i + pos), args[i]);
}
}catch (Exception e){
logger.error("日志记录失败:参数转换JSON出现异常",e);
}
return JSONObject.fromObject(map);
}
}
- 参数json工具类
/**
* 判断某个Json字符串是否为一个标准的Json字符串
* @param jsonString
* @return
*/
public static Boolean isJsonObject(String jsonString){
try{
JSONObject jsonObject = JSONObject.fromObject(jsonString);
return jsonObject != null && !jsonObject.isNullObject();
}catch (Exception e){
return false;
}
}
- IP地址工具类
public class IPaddressUtils {
/**
* 获取本机的内网ip地址
*
* @return
* @throws SocketException
*/
public static String getInnetIp(){
// IP
String ip_address = null;
try {
Enumeration<NetworkInterface> netInterfaces;
netInterfaces = NetworkInterface.getNetworkInterfaces();
InetAddress ip;
// 是否找到外网IP
boolean finded = false;
while (netInterfaces.hasMoreElements() && !finded) {
NetworkInterface ni = netInterfaces.nextElement();
Enumeration<InetAddress> address = ni.getInetAddresses();
while (address.hasMoreElements()) {
ip = address.nextElement();
// 外网IP
if (!ip.isSiteLocalAddress() && !ip.isLoopbackAddress() && !ip.getHostAddress().contains(":")){
ip_address = ip.getHostAddress();
finded = true;
break;
} else if (ip.isSiteLocalAddress() && !ip.isLoopbackAddress() && !ip.getHostAddress().contains(":")){
// 内网IP
ip_address = ip.getHostAddress();
}
}
}
} catch (Exception e) {
System.out.println("获取本地IP地址发生异常");
e.printStackTrace();
}
return ip_address;
}
/**
* 获取本机的外网ip地址
*
* @return
*/
public static String getV4IP() {
String ip = "";
String chinaz = "http://ip.chinaz.com";
StringBuilder inputLine = new StringBuilder();
String read = "";
URL url = null;
HttpURLConnection urlConnection;
BufferedReader in = null;
try {
url = new URL(chinaz);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), StandardCharsets.UTF_8));
while ((read = in.readLine()) != null) {
inputLine.append(read + "\r\n");
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Pattern p = Pattern.compile("\\<dd class\\=\"fz24\">(.*?)\\<\\/dd>");
Matcher m = p.matcher(inputLine.toString());
if (m.find()) {
String ipstr = m.group(1);
ip = ipstr;
}
return ip;
}
}
使用
@Override
@Log(operationType= OperationType.EDIT,module="会员中心",key = "name",describe="会员详细信息")
public JsonResult<CustInfoVO> editCustInfo(String token, String name, String startMonth, String endMonth){
//业务操作
}
最终记录的操作日志:
{
"id":"c170f2ad8a7c4b77969ad5fbf87c9b53",
"createTime":"2020-08-21 15:44:22",
"userId":"453a21c19e6543478dabc90342507037",
"userName":"管理员",
"userTel":"18888888888",
"module":"会员中心",
"operationType":"EDIT",
"operationTypeName":"编辑",
"keyName":"name",
"keyValues":"开卡记录",
"operationDescribe":"【会员中心】编辑【开卡记录】会员详细信息",
"method":"com.lvshou.mxgmember.service.CustomerService.saveCustomerBase",
"params":"{"data":"{\"birthday\":\"2020-07-06 00:00:00\",\"headImage\":null,\"name\":\"开卡记录\",\"phone\":\"132****8362\",\"customerTypeId\":\"476e36a9048e4e84b629dbc77201ea67\",\"referee\":\"6bcfd105980f44f1a8e557c244574c9f\",\"relationEmployee\":\"\",\"remark\":\"123\",\"sex\":2,\"sourceId\":\"625919a5fbc44576b5a89e09c88221c9\",\"address\":\"***\",\"provinceId\":\"4\",\"cityId\":\"56\",\"areaId\":\"550\"}","customerId":"28c7c01351c84047823b2209659edb7e","token":"a4f9bc36-2d40-48b0-83c7-e721cb34b999"}",
"dataId":null,
"ipAddress":"172.18.253.43"
}
总结
文章写到这,也就结束了,初次记录写文难免有不足,欢迎大家批评指正!