Java Web项目使用注解和面向切面编程优雅的记录操作日志

1.背景

在我们的项目中,记录模块的操作日志比较麻烦,需要通过定义对象,获取需要记录的日志内容,最后插入到日志操作表等一系列动作才能完成。该方案既笨拙,使用也不方便,使得代码中存在大量冗余。因此,想要找到一种方便又优雅的方案解决该问题,使得日志模块既不影响业务逻辑的执行,又能完善的记录系统操作日志。

最终,我们选择结合注解(Annotations)和面向切面编程(AOP)来记录操作日志的方案,该方案就是这样一种非常优雅且高效的方法。通过使用注解,可以标记需要记录日志的方法或类,而AOP则负责在运行时自动拦截这些被标记的组件,并执行日志记录的动作,使得日志逻辑和业务逻辑完美分离。

2.简介

注解:注解(Annotation)是一种代码级别的说明,也被称为元数据。有Spring使用经验的人应该都很熟悉,这边不再赘述。

AOP:面向切面编程(Aspect Oriented Programming),其概念是相对于我们比较熟悉OOP,即面向对象编程。AOP的优点很多,总结起来有如下几点:1.将与业务逻辑无关的通用逻辑代码单独提炼出来,减少这部分代码对业务逻辑本身的干扰;2.将通用业务逻辑代码提炼在一个模块中,偏便于代码维护;3.减少冗余代码的编写量;4.遵循OCP开闭原则,实现与关键代码的解耦,降低代码的耦合度。AOP在功能逻辑上最常用的的应用包括:操作日志记录、数据库事务管理和系统安全性验证等。

图1

传统的编程中,如图1所示,每一个服务逻辑(如:日志服务)都是单独的模块,业务模块(如:学生管理)需要使用这些服务,必须在模块中调用服务模块的接口,使得业务模块的代码中掺杂着与业务逻辑无关的服务模块的代码。这样使得业务模块变得复杂,不能适用大型系统的开发要求。

图2

引入AOP之后,如图2,我们在每个业务模块中找到合适的切面,将服务逻辑设计成单独并且可重复使用的切面逻辑,可以避免业务逻辑与服务逻辑的相互交织,使得服务逻辑能够灵活的应用到业务模块当中,并且在编写业务逻辑代码的时候并不需要关心服务模块的存在。

3.代码实现

3.1使用注解和AOP之前的代码

我们的项目使用的阿里云日志记录服务,在每个业务模块的操作方法中,都要加入封装好的日志记录模块代码,比如新闻公告模块中的修改新闻操作。

sendLog("修改新闻","[新闻公告]->[新闻管理]","修改新闻信息,标题:"+newsInfo.getNews_title());

sendLog方法实现在公用Controller父类,代码如下:

/**
	 * 日志发送
	 * 日志发送写在方法最前面
	 * 查询不记录日志
	 * @param operTopic 用户操作主题 如:一对一排课
	 * @param operContent 用户操作内容  如:姓名:周倪暄,班级:精品一对一(初一)_周倪暄,老师:朱婷......
	 * @param operContentType 内容类型 json / text  默认 text
	 */
	public void sendLog(String operTopic,String operLocation,String operContent,String operContentType) throws Exception {
		AliyunLogEntity aliyunLogEntity = new AliyunLogEntity();
		aliyunLogEntity.setOrg_code(get_login_user().getIdentity_code());
		aliyunLogEntity.setDept_code(get_dpt().getCode());
		aliyunLogEntity.setUser_name(get_login_user().getPerson_name());
		aliyunLogEntity.setOper_topic(operTopic);
		aliyunLogEntity.setOper_content(operContent);
		aliyunLogEntity.setOper_location(operLocation);
		aliyunLogEntity.setOper_content_type(operContentType);
		aliyunLogEntity.setUser_number(get_login_user().getUser_code());
		AliyunLogUtil.sendLog(aliyunLogEntity);
	}

AliyunLogUtil工具类如下:

package com.framework.logger;

import com.aliyun.openservices.log.Client;
import com.aliyun.openservices.log.common.LogContent;
import com.aliyun.openservices.log.common.LogItem;
import com.aliyun.openservices.log.common.QueriedLog;
import com.aliyun.openservices.log.exception.LogException;
import com.aliyun.openservices.log.producer.LogProducer;
import com.aliyun.openservices.log.producer.ProducerConfig;
import com.aliyun.openservices.log.producer.ProjectConfig;
import com.aliyun.openservices.log.request.GetHistogramsRequest;
import com.aliyun.openservices.log.request.GetLogsRequest;
import com.aliyun.openservices.log.response.GetHistogramsResponse;
import com.aliyun.openservices.log.response.GetLogsResponse;
import com.framework.thread.ThreadPoolUtil;
import com.framework.util.DateUtil;
import com.framework.util.RequestUtil;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.util.*;

/**
 * 阿里云操作日志记录(tw-logstore)
 * @author Bill Fang
 * @time 2023-07-23
 */

public class AliyunLogUtil {
    //日志项目(必选)
    static String projectName = "tw-logservice";
    //日志仓库(必选)
    static String logStore = "tw-logstore";
    //日志主题(可选)
    static String topic = "topic0";
    //日志服务器地址(必选)
    static String endpoint = "***********";
    //申请访问阿里云的ID(必选)
    static String accessKeyId = "***********";
    //申请访问阿里云的密码(必选)
    static String accessKey = "***************";

    //日志生产者配置
    static ProducerConfig producerConfig = new ProducerConfig();
    //初始化日志生产者
    static LogProducer producer = new LogProducer(producerConfig);
    //日志链接配置
    static ProjectConfig projectConfig = new ProjectConfig(projectName,endpoint,accessKeyId,accessKey);
    // 构建一个客户端实例
    private static Client client;

    //线程池
    private static final ThreadPoolUtil threadPoolUtil = new ThreadPoolUtil();

    static {
        //配置阿里云的日志链接
        producer.setProjectConfig(projectConfig);
        client = new Client(endpoint, accessKeyId, accessKey);
    }

    /**
     * 日志发送
     */
    public static void sendLog(AliyunLogEntity aliyunLogEntity) {
        String ip = getIp();
        Vector<LogItem> tmpLogGroup = new Vector<>();
        LogItem logItem = new LogItem((int) (new Date().getTime() / 1000));
        logItem.PushBack("level", "INFO");
        logItem.PushBack("method", AliyunLogUtil.class.toString()+"."+"sendLog()");
        //机构号
        if(aliyunLogEntity.getOrg_code() != null){
            logItem.PushBack("org_code", aliyunLogEntity.getOrg_code());
        }
        //部门编号
        if(aliyunLogEntity.getDept_code() != null){
            logItem.PushBack("dept_code", aliyunLogEntity.getDept_code());
        }
        //用户名称
        if(aliyunLogEntity.getUser_name() != null){
            logItem.PushBack("user_name", aliyunLogEntity.getUser_name());
        }
        //用户工号或ID
        if(aliyunLogEntity.getUser_number() != null){
            logItem.PushBack("user_number",aliyunLogEntity.getUser_number());
        }
        //操作描述
        if(aliyunLogEntity.getOper_topic() != null){
            logItem.PushBack("oper_topic",aliyunLogEntity.getOper_topic());
        }
        //操作内容
        if(aliyunLogEntity.getOper_content() != null){
            logItem.PushBack("oper_content", aliyunLogEntity.getOper_content());
        }
        //操作位置
        if(aliyunLogEntity.getOper_location() != null){
            logItem.PushBack("oper_location", aliyunLogEntity.getOper_location());
        }
        //操作内容类型 json / text  默认是text
        if(aliyunLogEntity.getOper_content_type() != null){
            logItem.PushBack("oper_content_type", aliyunLogEntity.getOper_content_type());
        }else{
            logItem.PushBack("oper_content_type", "text");
        }
        //操作IP
        logItem.PushBack("oper_ip",ip);
        //操作时间
        logItem.PushBack("time", DateUtil.getLocalDateTime());
        tmpLogGroup.add(logItem);
        //执行发送日志线程
        threadPoolUtil.execute(new AliyunLogRunnable(projectName, logStore, topic, ip, tmpLogGroup, producer));
    }

    /**
     * 返回查询条件
     * @param aliyunLogEntity
     * @return
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public static String getQuerySql(AliyunLogEntity aliyunLogEntity) throws NoSuchFieldException, IllegalAccessException {
        StringBuilder stringBuilder = new StringBuilder();
        Class<? extends AliyunLogEntity> clazz = aliyunLogEntity.getClass();
        //模糊查询的字段
        Field likeField = clazz.getDeclaredField("queryLikeData");
        likeField.setAccessible(true);
        List likeList = (List) likeField.get(aliyunLogEntity);
        Field[] fields = clazz.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            field.setAccessible(true);
            if(!field.getName().equals("queryLikeData")){
                Object value = field.get(aliyunLogEntity);
                if(value != null && !String.valueOf(value).equals("")){
                    stringBuilder.append(field.getName());
                    //匹配模糊查询的sql
                    if(likeList.contains(field.getName())){
                        stringBuilder.append(":");
                        stringBuilder.append(value);
                    }else{
                        stringBuilder.append("=");
                        stringBuilder.append(value);
                    }
                    stringBuilder.append(" and ");
                }
            }
        }
        String result = stringBuilder.toString();
        if(!StringUtils.isEmpty(result)){
            result = result.substring(0,result.length()-5);
        }
        return result;
    }

    /**
     * 获取日志查询时间
     * @param isFormat 是否格式化  true: 返回格式化时间  false:返回long时间
     * @return 返回时间格式 “yyyy-MM-dd HH:mm:ss”
     */
    public static Map<String, String> getTimesMap(boolean isFormat) {
        return getTimesMap(isFormat,null);
    }

    //时间转换
    private static String convertTime(Date date, String format){
        String t1 = DateUtil.fomatDate(date,"yyyy-MM-dd");
        Date t2 = DateUtil.fomatDate(t1, format);
        return DateUtil.fomatDate(t2, format);
    }



    /**
     * 获取日志查询时间
     * @param isFormat 是否格式化  true: 返回格式化时间  false:返回long时间
     * @param format 默认 “yyyy-MM-dd HH:mm:ss”
     * @return {
     *              now: 当前时间,
     *              day: 三天前时间,
     *              week: 一周前时间,
     *              month: 一个月前时间
     *          }
     */
    public static Map<String, String> getTimesMap(boolean isFormat, String format) {
        format = format == null ? "yyyy-MM-dd HH:mm:ss" : format;
        Calendar calendar = Calendar.getInstance();
        Map<String, String> timeMap = new HashMap<>();
        //返回当前时间最后的时间 23:59:59
        Date date =DateUtil.fomatDate(DateUtil.getTimeTo59(new Date()), "yyyy-MM-dd HH:mm:ss");
        calendar.setTime(date);
        //现在的时间
        if (!isFormat) {
            timeMap.put("now", String.valueOf(DateUtil.long13To10(calendar.getTimeInMillis())));
        } else {
            timeMap.put("now", DateUtil.fomatDate(new Date(calendar.getTimeInMillis()), format));
        }
        //三天前的时间
        calendar.setTime(new Date());
        calendar.add(Calendar.DATE, -3);
        String time = DateUtil.fomatDate(new Date(calendar.getTimeInMillis()), DateUtil.YYYY_MM_DD);
        if (!isFormat) {
            timeMap.put("day",String.valueOf(DateUtil.stringTo10(time,DateUtil.YYYY_MM_DD)));
        } else {
            timeMap.put("day",time);
        }
        //一周前的时间
        calendar.setTime(new Date());
        calendar.add(Calendar.DATE, -7);
        time = DateUtil.fomatDate(new Date(calendar.getTimeInMillis()), DateUtil.YYYY_MM_DD);
        if (!isFormat) {
            timeMap.put("week",String.valueOf(DateUtil.stringTo10(time,DateUtil.YYYY_MM_DD)));
        } else {
            timeMap.put("week", time);
        }
        //一个月前的时间
        calendar.setTime(new Date());
        calendar.add(Calendar.MONTH, -1);
        time = DateUtil.fomatDate(new Date(calendar.getTimeInMillis()), DateUtil.YYYY_MM_DD);
        if (!isFormat) {
            timeMap.put("month", String.valueOf(DateUtil.stringTo10(time,DateUtil.YYYY_MM_DD)));
        } else {
            timeMap.put("month", time);
        }

        return timeMap;
    }


    /**
     * 统计符合条件的日志的记录条数
     * <br/><b>只有阿里云日志控制台打开索引功能,才能使用此接口</b>
     * <br/>最新数据有1分钟延迟时间
     * @param query 查询表达式。为空字符串""则查询所有。“=”也支持模糊查询“*”,关于查询表达式的详细语法,请参考 <a href="https://help.aliyun.com/document_detail/29060.html">查询语法 https://help.aliyun.com/document_detail/29060.html</a>
     * @param topic 查询日志主题。为空字符串""则查询所有。
     * @param startTime 查询开始时间点(精度为秒,从 1970-1-1 00:00:00 UTC 计算起的秒数)。10位时间戳,可用 DateUtil.timeForUnix10() 取得  或通过 DateUtil.long13To10()转换
     * @param endTime 查询结束时间点,10位时间戳,可用 DateUtil.timeForUnix10() 取得 或通过 DateUtil.long13To10()转换
     * @param offset 请求返回日志的起始点。取值范围为 0 或正整数,默认值为 0。
     * @param line 请求返回的最大日志条数。取值范围为 0~100,默认值为 100。
     * @return {@link com.aliyun.openservices.log.common.QueriedLog}数组,将每条日志内容都详细列出来。若返回null,则是读取失败
     * @throws com.aliyun.openservices.log.exception.LogException
     */
    public static List<AliyunLogEntity> queryLogs(int startTime, int endTime, int offset, int line, String query) throws LogException {
        List<AliyunLogEntity> resultList = new ArrayList<>();
        GetLogsResponse res4 = null;
        //对于每个 log offset,一次读取 10 行 log,如果读取失败,最多重复读取 3 次。
        for (int retry_time = 0; retry_time < 3; retry_time++) {
            //true : 逆序,时间越大越靠前  false : 顺序,时间越大越靠后
            boolean reverse = true;
            GetLogsRequest req4 = new GetLogsRequest(projectName, logStore, startTime, endTime,topic, query, offset, line, reverse);
            res4 = client.GetLogs(req4);
            if (res4 != null && res4.IsCompleted()) {
                break;
            }
        }
        if(res4 == null){
            return null;
        }
        ArrayList<QueriedLog> list = res4.GetLogs();
        for (QueriedLog queriedLog : list) {
            AliyunLogEntity aliyunLogEntity = new AliyunLogEntity();
            LogItem item = queriedLog.GetLogItem();
            ArrayList<LogContent> contents = item.mContents;
            for (LogContent content : contents) {
                String key = content.GetKey();
                String value = content.GetValue();
                value = value == null?"":value;
                switch (key){
                    case "org_code":
                        aliyunLogEntity.setOrg_code(value);
                        break;
                    case "dept_code":
                        aliyunLogEntity.setDept_code(value);
                        break;
                    case "user_name":
                        aliyunLogEntity.setUser_name(value);
                        break;
                    case "user_number":
                        aliyunLogEntity.setUser_number(value);
                        break;
                    case "oper_topic":
                        aliyunLogEntity.setOper_topic(value);
                        break;
                    case "oper_content":
                        aliyunLogEntity.setOper_content(value);
                        break;
                    case "oper_location":
                        aliyunLogEntity.setOper_location(value);
                        break;
                    case "oper_content_type":
                        aliyunLogEntity.setOper_content_type(value);
                        break;
                    case "oper_ip":
                        aliyunLogEntity.setOper_ip(value);
                        break;
                    case "time":
                        aliyunLogEntity.setTime(value);
                        break;
                }
            }
            resultList.add(aliyunLogEntity);
        }
        return resultList;
    }


    /**
     * 根据条件查询总个数
     * @param startTime 开始时间
     * @param endTime 结束时间
     * @param query 查询sql
     * @return 分页总数
     * @throws LogException
     */
    public static int queryLogsCount(int startTime,int endTime,String query) throws LogException {
        System.out.println(DateUtil.fomatDate(new Date(startTime*1000L)));
        System.out.println("-");
        System.out.println(DateUtil.fomatDate(new Date(endTime*1000L)));
        GetHistogramsRequest getHistogramsRequest = new GetHistogramsRequest(projectName,logStore,topic,query,startTime,endTime);
        GetHistogramsResponse result = client.GetHistograms(getHistogramsRequest);
        if(result == null){
            return 0;
        }
        return (int) result.GetTotalCount();
    }

    /**
     * 获取ip
     */
    private static String getIp() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return "127.0.0.1";
        } else {
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            String ip = RequestUtil.getIpAddr(request);
            return ip;
        }
    }
}

使用该方案的操作日志记录服务,虽然代码冗余也比较少,但是每个服务模块的操作代码中都要调用sendLog方法才能记录操作日志,较为繁琐,并且日志服务代码和业务逻辑代码交织在一起,不够优雅。

3.2使用注解和AOP改造操作日志模块

3.2.1日志操作表结构

CREATE TABLE `sys_oper_log` (
  `code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '日志主键',
  `title` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '模块标题',
  `business_type` int(2) DEFAULT '0' COMMENT '业务类型',
  `business_type_name` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '业务类型名称',
  `operator_type` int(1) DEFAULT '0' COMMENT '操作类别(0其它 1后台用户 2微信)',
  `method` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '方法名称',
  `request_method` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '请求方式',
  `user_code` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户主键后台为sys_user主键 微信为wx_base_user主键',
  `user_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '操作人员',
  `org_code` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '机构主键',
  `oper_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '请求URL',
  `user_ip` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '主机地址',
  `oper_location` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '操作地点',
  `oper_param` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '请求参数',
  `json_result` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '返回参数',
  `status` int(1) DEFAULT '0' COMMENT '操作状态(0正常 1异常)',
  `error_msg` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '错误消息',
  `oper_time` int(11) DEFAULT '0' COMMENT '处理时长,毫秒',
  `create_time` datetime DEFAULT NULL COMMENT '操作时间',
  PRIMARY KEY (`code`),
  KEY `time_key` (`create_time`) USING BTREE,
  KEY `title_index` (`title`) USING BTREE,
  KEY `oper_time_index` (`oper_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志记录';

所有的系统操作日志都记录在该表中,包含业务类型、业务名称、操作类别、调用方法、操作员主键、机构名称等等信息。

3.2.2注解实现

废话不多说,直接上代码:

package com.framework.annotation;

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;

import com.framework.enums.BusinessType;
import com.framework.enums.OperatorType;

@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented //生成文档
public @interface SysLog {
	/**
     * 模块 
     */
    public String title() default "";

    /**
     * 功能
     */
    public BusinessType businessType() default BusinessType.QUERY;

    /**
     * 操作人类别
     */
    public OperatorType operatorType() default OperatorType.MANAGE;

    /**
     * 是否保存请求的参数
     */
    public boolean isSaveRequestData() default true;
    
}

这个@SysLog注解定义了一个用于系统日志记录的注解,它可以在方法级别上使用。这个注解的设计目的是为了方便开发者在需要记录日志的方法上添加元数据信息,如模块标题、业务类型、操作人类别以及是否保存请求参数等。下面是对这个注解中各个部分的详细解释:

  1. @Target(ElementType.METHOD): 这个元注解指定了@SysLog注解的应用目标。在这个例子中,它被设置为ElementType.METHOD,意味着这个注解只能被应用于方法上。这是通过Java的元注解机制实现的,用于定义注解的使用范围。

  2. @Retention(RetentionPolicy.RUNTIME): 这个元注解指定了注解的保留策略。RetentionPolicy.RUNTIME意味着注解不仅被保留在.class文件中,而且在运行时可以通过反射被读取。这对于需要在运行时动态获取注解信息的场景非常有用,比如在AOP(面向切面编程)中自动记录日志。

  3. @Documented: 这个元注解表明被注解的元素应该被javadoc或类似的工具文档化。也就是说,当你使用javadoc生成API文档时,这个注解会被包含在生成的文档中。

  4. 注解属性:

    • title(): 一个返回String类型的属性,用于指定模块的标题。它有一个默认值"",即如果在使用注解时没有指定title,那么它将被视为空字符串。
    • businessType(): 一个返回BusinessType枚举类型的属性,用于指定业务类型。它同样有一个默认值BusinessType.QUERY,这意呀着如果未指定,则默认业务类型为查询。
    • operatorType(): 一个返回OperatorType枚举类型的属性,用于指定操作人类别。默认值为OperatorType.MANAGE,表示如果未指定,则默认为管理类操作。
    • isSaveRequestData(): 一个返回boolean类型的属性,用于指定是否保存请求的参数。默认值为true,意味着如果未指定,则默认保存请求参数。

3.2.3切面实现

package com.framework.aspectj;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.Map;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
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.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.biz.entity.org.WxBaseUser;
import com.biz.entity.system.SysOperLog;
import com.biz.entity.system.User;
import com.biz.service.org.WxBaseUserService;
import com.biz.service.system.SysOperLogService;
import com.biz.service.util.RedisManageUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.framework.annotation.SysLog;
import com.framework.util.DateUtil;
import com.framework.util.MyNumber;
import com.framework.util.MyTools;
import com.framework.util.ServletUtils;
import com.framework.util.StringUtil;
import com.framework.util.UuidUtil;

@Aspect
@Component
public class LogAspect {
	private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

	@Resource(name = "sysOperLogService")
	protected SysOperLogService sysOperLogService;
	@Resource(name = "wxBaseUserService")
	protected WxBaseUserService wxBaseUserService;
	@Autowired
	protected RedisManageUtils redisManageUtils;

	// 配置织入点
	@Pointcut("@annotation(com.framework.annotation.SysLog)")
	public void logPointCut() {

	}

	// 环绕通知
	@Around("logPointCut()")
	public Object around(ProceedingJoinPoint joinPoint) {
		Object result = null;
		long startTimeMillis = System.currentTimeMillis();
		// 保存日志进系统
		SysOperLog sysOperLog = new SysOperLog();

		try {
			// 设置方法名称
			String className = joinPoint.getTarget().getClass().getName();
			String methodName = joinPoint.getSignature().getName();
			// 获得注解
			SysLog sysLog = getAnnotationLog(joinPoint);

			sysOperLog.setCode(MyNumber.getTimeCode(11)); // 日志主键
			sysOperLog.setTitle(StringUtils.trimToEmpty(sysLog.title())); // 模块标题
			sysOperLog.setBusiness_type(sysLog.businessType().getBusinessType()); // 业务类型
			sysOperLog.setBusiness_type_name(sysLog.businessType().getBusinessTypeName()); // 业务类型名称
			sysOperLog.setOperator_type(sysLog.operatorType().ordinal()); // 操作类别(0-后台项目 1-微信项目)
			sysOperLog.setMethod(className + "." + methodName + "()"); // 方法名称
			sysOperLog.setRequest_method(ServletUtils.getRequest().getMethod()); // 请求方式

			// 获取当前的用户
			dealOperUser(sysLog, sysOperLog, joinPoint);

			// 是否需要保存request,参数和值
			if (sysLog.isSaveRequestData()) {
				setRequestValue(sysOperLog, joinPoint);
			}

			sysOperLog.setOper_url(ServletUtils.getRequest().getRequestURL().toString()); // 请求URL
			sysOperLog.setUser_ip(getVistIP()); // 主机地址
			sysOperLog.setCreate_time(DateUtil.getTime()); // 操作时间

			// 调用 proceed() 方法才会真正的执行实际被代理的方法
			result = joinPoint.proceed();

			String json_result = "";
			try {
				ObjectMapper objectMapper = new ObjectMapper();
				ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
				String resultStr = objectWriter.writeValueAsString(result);
				json_result = StringUtil.subString(resultStr, 0, 2000);
			} catch (Exception e) {
				json_result = "获取返回参数异常";
			}

			sysOperLog.setStatus(0); //操作状态(0正常 1异常)
			sysOperLog.setJson_result(json_result); //返回参数
			long execTimeMillis = System.currentTimeMillis() - startTimeMillis;
			sysOperLog.setOper_time(execTimeMillis); //处理时长,毫秒
			sysOperLogService.insertSysOperLog(sysOperLog);

		} catch (Throwable e) {

			StringWriter sw = new StringWriter();
			e.printStackTrace(new PrintWriter(sw, true));
			String error_msg = sw.toString().substring(0,2000);

			sysOperLog.setStatus(1); //操作状态(0正常 1异常)
			sysOperLog.setJson_result("异常报错"); //返回参数
			long execTimeMillis = System.currentTimeMillis() - startTimeMillis;
			sysOperLog.setOper_time(execTimeMillis); //处理时长,毫秒
			sysOperLog.setError_msg(error_msg); //错误消息
			try {
				sysOperLogService.insertSysOperLog(sysOperLog);
			} catch (Exception e1) {
				e1.printStackTrace();
				throw new RuntimeException(e);
			}

			throw new RuntimeException(e);
		}
		return result;
	}


	/**
	 * 拦截异常操作
	 *
	 * @param joinPoint 切点
	 * @param e 异常
	 */
	@AfterThrowing(value = "logPointCut()", throwing = "e")
	public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
		handleLog(joinPoint, e, null);
	}

	private void handleLog(JoinPoint joinPoint, Exception e, Object jsonResult) {

		try {
			// 获得注解
			SysLog controllerLog = getAnnotationLog(joinPoint);
			if (controllerLog == null) {
				return;
			}
			// *========数据库日志=========*//
			SysOperLog operLog = new SysOperLog();
			operLog.setCode(UuidUtil.get32UUID());
			operLog.setStatus(1);// 状态正常
			// 请求的地址
			operLog.setUser_ip(getVistIP());
			// 返回参数
			ObjectMapper objectMapper = new ObjectMapper();
			ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
			String resultStr = objectWriter.writeValueAsString(jsonResult);
			operLog.setJson_result(StringUtil.subString(resultStr, 0, 2000));

			operLog.setOper_url(ServletUtils.getRequest().getRequestURL().toString());
			// 获取当前的用户
			dealOperUser(controllerLog, operLog, joinPoint);

			// 记录下请求内容
			if (e != null) {
				operLog.setStatus(0);// 状态异常
				if (!StringUtil.isEmpty(getTrace(e))) {
					operLog.setError_msg(getTrace(e).length() <= 2000 ? getTrace(e) : getTrace(e).substring(0, 2000));
				}
			}
			// 设置方法名称
			String className = joinPoint.getTarget().getClass().getName();
			String methodName = joinPoint.getSignature().getName();
			operLog.setMethod(className + "." + methodName + "()");
			// 设置请求方式
			operLog.setRequest_method(ServletUtils.getRequest().getMethod());
			// 处理设置注解上的参数
			getControllerMethodDescription(controllerLog, operLog, joinPoint);

			operLog.setCreate_time(DateUtil.getTime());
			// 保存数据库
			// sysOperLogService.insertSysOperLog(operLog);

		} catch (Exception exp) {
			log.error("==前置通知异常==");
			log.error("异常信息:{}", exp);
			exp.printStackTrace();
		}
	}

	/**
	 * 处理登录用户
	 */
	private void dealOperUser(SysLog controllerLog, SysOperLog operLog, JoinPoint joinPoint) throws Exception {
		if (controllerLog.operatorType().ordinal() == 0) {// 后台
			User currentUser = (User) ServletUtils.getRequest().getSession().getAttribute("login_user");
			if (currentUser != null) {
				operLog.setUser_name(currentUser.getLogin_name());
				operLog.setUser_code(currentUser.getUser_code());
				if (!StringUtil.isEmpty(currentUser.getIdentity_code())) {
					operLog.setOrg_code(currentUser.getIdentity_code());
				} else {
					operLog.setOrg_code("admin");
				}
			}
		} else if (controllerLog.operatorType().ordinal() == 1) {// 微信端
			String app_id = "";

			HttpServletRequest request = ServletUtils.getRequest();
			String params = "";
			@SuppressWarnings("unchecked")
			Map<String, String[]> map = request.getParameterMap();
			if (map == null || map.size() == 0) {
				params = JSONObject.toJSONString(joinPoint.getArgs());
				JSONArray jsonArray = JSON.parseObject(params, JSONArray.class);
				if (jsonArray != null && jsonArray.size() > 0) {
					JSONObject json = JSON.parseObject(jsonArray.getString(0), JSONObject.class);
					if (json != null && json.containsKey("app_id")) {
						app_id = json.getString("app_id");
					}
				}
			} else {
				ObjectMapper objectMapper = new ObjectMapper();
				ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
				params = objectWriter.writeValueAsString(map);
				if (map.containsKey("app_id")) {
					String[] tmpArr = map.get("app_id");
					if (tmpArr != null && tmpArr.length > 0) {
						app_id = tmpArr[0];
					}
				}
			}

			if (!StringUtil.isEmpty(app_id)) {

				String sub_openid = (String) request.getSession().getAttribute(app_id + "_open_id");
				if (!StringUtil.isEmpty(sub_openid)) {
					WxBaseUser wxBaseUser = wxBaseUserService.getWxBaseUserById(app_id, sub_openid);
					if (wxBaseUser != null) {// 登录用户
						String roleName = "";
						if (wxBaseUser.getType() == 0) {// 教师
							roleName = "老师";
						}
						if (wxBaseUser.getType() == 1) {// 家长
							roleName = "家长";
						}
						operLog.setUser_name(wxBaseUser.getName() + "[" + roleName + "]");
						operLog.setUser_code(wxBaseUser.getCode());
						operLog.setOrg_code(wxBaseUser.getOrg_code());
					} else {// 游客
						Map<String, String> orgMap = wxBaseUserService.getOrganizeMapByAppId(app_id);
						if (orgMap != null && orgMap.size() > 0 && orgMap.containsKey("org_code")) {
							operLog.setUser_name("游客");
							operLog.setOrg_code(orgMap.get("org_code"));
						}
					}
				}
			}
		}
	}

	/**
	 * 是否存在注解,如果存在就获取
	 */
	private SysLog getAnnotationLog(JoinPoint joinPoint) throws Exception {
		Signature signature = joinPoint.getSignature();
		MethodSignature methodSignature = (MethodSignature) signature;
		Method method = methodSignature.getMethod();

		if (method != null) {
			return method.getAnnotation(SysLog.class);
		}
		return null;
	}

	public String getVistIP() {
		HttpServletRequest request = ServletUtils.getRequest();
		String ip = "";
		if (request.getHeader("x-forwarded-for") == null) {
			ip = request.getRemoteAddr();
		} else {
			ip = request.getHeader("x-forwarded-for");
		}
		return ip;
	}

	/**
	 * 获取注解中对方法的描述信息 用于Controller层注解
	 *
	 * @param log 日志
	 * @param operLog 操作日志
	 * @param joinPoint
	 * @throws Exception
	 */
	public void getControllerMethodDescription(SysLog log, SysOperLog operLog, JoinPoint joinPoint) throws Exception {
		// 设置action动作
		operLog.setBusiness_type(log.businessType().ordinal());
		// 设置标题
		operLog.setTitle(log.title());
		// 设置操作人类别
		operLog.setOperator_type(log.operatorType().ordinal());
		// 是否需要保存request,参数和值
		if (log.isSaveRequestData()) {
			// 获取参数的信息,传入到数据库中。
			setRequestValue(operLog, joinPoint);
		}
	}

	/**
	 * 获取请求的参数,放到log中
	 *
	 * @param operLog 操作日志
	 * @param joinPoint
	 * @throws Exception 异常
	 */
	private void setRequestValue(SysOperLog operLog, JoinPoint joinPoint){
		String params = "";
		String class_code = "";
		try {
			HttpServletRequest request = ServletUtils.getRequest();
			@SuppressWarnings("unchecked")
			Map<String, String[]> param_map = request.getParameterMap();
			if (param_map == null || param_map.size() == 0) {
				Object[] args = joinPoint.getArgs();
				if(args!= null && args.length > 0){
					try {
						params = JSONObject.toJSONString(args);
					} catch (Exception e) {

					}
				}
			} else {

				for (String key : param_map.keySet()) {

					String[] values = param_map.get(key);
					for (int i = 0; i < values.length; i++) {
						String value = values[i];
						params = MyTools.connect(params, key + "=" + value, "&");
					}
				}
			}
			params = StringUtil.subString(params, 0, 1000);
		} catch (Exception e) {
			params = "获取请求参数异常";
		}
		operLog.setOper_param(params);
	}

	public static String getTrace(Throwable t) {
		StringWriter stringWriter = new StringWriter();
		PrintWriter writer = new PrintWriter(stringWriter);
		t.printStackTrace(writer);
		StringBuffer buffer = stringWriter.getBuffer();
		return buffer.toString();
	}

}

这里的@Pointcut("@annotation(com.framework.annotation.SysLog)")是在方法的@SysLog注解处织入切面,并且该切面的主要逻辑实现在环绕通知的around方法中,在该方法中主要记录操作人信息,操作的方法,执行时间,执行参数等信息。

3.2.3日志服务的使用

完成上述工作之后,在需要记录操作日志的方法前,加入@SysLog注解即可实现操作日志的记录,具体使用方法如下:

通过以上方案实现了使用注解和面向切面编程优雅的记录系统操作日志的工作。无需在每个方法中编写重复的日志记录代码。这种方法不仅提高了代码的可读性和可维护性,还增强了系统的可扩展性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值