使用AOP实现日志管理

在这里插入图片描述

自定义注解

package com.yangzc.studentboot.common.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.lang.annotation.RetentionPolicy;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
	String value() default "";
}

给登录接口添加注解

    @Log("登录")
    @PostMapping("/login")
    @ResponseBody
    R ajaxLogin(String username, String password, String verify, HttpServletRequest request) {
        try {
            //从session中获取随机数
            String random = (String) request.getSession().getAttribute(RandomValidateCodeUtil.RANDOMCODEKEY);
            if (StringUtils.isBlank(verify)) {
                return R.error("请输入验证码");
            }
            if (random.equals(verify)) {
            } else {
                return R.error("请输入正确的验证码");
            }
        } catch (Exception e) {
            logger.error("验证码校验失败", e);
            return R.error("验证码校验失败");
        }
        password = MD5Utils.encrypt(username, password);
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            return R.ok();
        } catch (AuthenticationException e) {
            return R.error("用户或密码错误");
        }
    }

使用Aspect记录操作日志

package com.yangzc.studentboot.common.aspect;

import java.lang.reflect.Method;
import java.util.Date;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import com.yangzc.studentboot.common.service.LogService;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.yangzc.studentboot.common.annotation.Log;
import com.yangzc.studentboot.common.domain.LogDO;
import com.yangzc.studentboot.common.utils.HttpContextUtils;
import com.yangzc.studentboot.common.utils.IPUtils;
import com.yangzc.studentboot.common.utils.JSONUtils;
import com.yangzc.studentboot.common.utils.ShiroUtils;
import com.yangzc.studentboot.system.domain.UserDO;
import org.springframework.web.multipart.MultipartFile;

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

    @Autowired
    LogService logService;


    @Pointcut("@annotation(com.yangzc.studentboot.common.annotation.Log)")
    public void logPointCut() {
    }

    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        long beginTime = System.currentTimeMillis();
        // 执行方法
        Object result = point.proceed();
        // 执行时长(毫秒)
        long time = System.currentTimeMillis() - beginTime;
        //异步保存日志
        saveLog(point, time);
        return result;
    }

    void saveLog(ProceedingJoinPoint joinPoint, long time) throws InterruptedException {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogDO sysLog = new LogDO();
        Log syslog = method.getAnnotation(Log.class);
        if (syslog != null) {
            // 注解上的描述
            sysLog.setOperation(syslog.value());
        }
        // 请求的方法名
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        sysLog.setMethod(className + "." + methodName + "()");
        // 请求的参数
        Object[] args = joinPoint.getArgs();
        Object[] arguments  = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) {
                //ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
                //ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
                continue;
            }
            arguments[i] = args[i];
        }
        try {
            String params = JSONUtils.beanToJson(arguments);
            sysLog.setParams(params);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 获取request
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        // 设置IP地址
        sysLog.setIp(IPUtils.getIpAddr(request));
        // 用户名
        UserDO currUser = ShiroUtils.getUser();
        if (null == currUser) {
            if (null != sysLog.getParams()) {
                sysLog.setUserId(-1L);
                sysLog.setUsername(sysLog.getParams());
            } else {
                sysLog.setUserId(-1L);
                sysLog.setUsername("获取用户信息为空");
            }
        } else {
            sysLog.setUserId(ShiroUtils.getUserId());
            sysLog.setUsername(ShiroUtils.getUser().getUsername());
        }
        sysLog.setTime((int) time);
        // 系统当前时间
        Date date = new Date();
        sysLog.setCreateOn(date);
        // 保存系统日志
        logService.save(sysLog);
    }
}

日志查看接口

package com.yangzc.studentboot.common.controller;

import java.util.Map;

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.yangzc.studentboot.common.domain.LogDO;
import com.yangzc.studentboot.common.domain.PageDO;
import com.yangzc.studentboot.common.service.LogService;
import com.yangzc.studentboot.common.utils.Query;
import com.yangzc.studentboot.common.utils.R;

@RequestMapping("/log")
@Controller
public class LogController {
	@Autowired
	LogService logService;
	String prefix = "log";

	@RequiresPermissions("log:list")
	@GetMapping("/list")
	String log() {
		return prefix + "/log";
	}

	@RequiresPermissions("log:list")
	@ResponseBody
	@GetMapping("/data")
	PageDO<LogDO> list(@RequestParam Map<String, Object> params) {
		Query query = new Query(params);
		PageDO<LogDO> page = logService.queryList(query);
		return page;
	}
	
	@ResponseBody
	@PostMapping("/remove")
	R remove(Long id) {
		if (logService.remove(id)>0) {
			return R.ok();
		}
		return R.error();
	}

	@ResponseBody
	@PostMapping("/batchRemove")
	R batchRemove(@RequestParam("ids[]") Long[] ids) {
		int r = logService.batchRemove(ids);
		if (r > 0) {
			return R.ok();
		}
		return R.error();
	}
}

日志查看页面(html)

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<meta charset="utf-8">
<head th:include="include :: header"></head>
<body class="gray-bg">
	<div class="wrapper wrapper-content">
		<div class="col-sm-12">
			<div class="ibox">
				<div class="ibox-body">
					<div class="fixed-table-toolbar">
						<div class="columns pull-left">
							<button type="button" class="btn  btn-danger"
								onclick="batchRemove()">
								<i class="fa fa-trash" aria-hidden="true"></i>删除
							</button>
						</div>
						<div class="columns pull-right" role="group">
							<button class="btn btn-success" onclick="reLoad()">
								<i class="fa fa-search" aria-hidden="true"></i>查询
							</button>
						</div>
						<div class="pull-right search col-md-2">
							<input id="searchOperation" type="text" class="form-control"
								placeholder="操作">
						</div>
						<div class="pull-right search col-md-2 nopadding">
							<input id="searchUsername" type="text" class="form-control"
								placeholder="用户">
						</div>
					</div>
					<table id="exampleTable" data-mobile-responsive="true">
					</table>
				</div>
			</div>
		</div>
	</div>
	<div th:include="include :: footer"></div>
	<script type="text/javascript" src="/app/log/log.js"></script>
</body>
</html>

日志查看页面(js)

var prefix = "/log"
$(function () {
    load();

});
$('#exampleTable').on('load-success.bs.table', function (e, data) {
    if (data.total && !data.rows.length) {
        $('#exampleTable').bootstrapTable('selectPage').bootstrapTable('refresh');
    }
});

function load() {
    $('#exampleTable')
        .bootstrapTable(
            {
                method: 'get', // 服务器数据的请求方式 get or post
                url: prefix + "/data", // 服务器数据的加载地址
                // showRefresh : true,
                // showToggle : true,
                // showColumns : true,
                iconSize: 'outline',
                toolbar: '#exampleToolbar',
                striped: true, // 设置为true会有隔行变色效果
                dataType: "json", // 服务器返回的数据类型
                pagination: true, // 设置为true会在底部显示分页条
                // queryParamsType : "limit",
                // //设置为limit则会发送符合RESTFull格式的参数
                singleSelect: false, // 设置为true将禁止多选
                // contentType : "application/x-www-form-urlencoded",
                // //发送到服务器的数据编码类型
                pageSize: 10, // 如果设置了分页,每页数据条数
                pageNumber: 1, // 如果设置了分布,首页页码
                // search : true, // 是否显示搜索框
                // showColumns : true, // 是否显示内容下拉框(选择显示的列)
                sidePagination: "server", // 设置在哪里进行分页,可选值为"client" 或者
                // "server"
                queryParams: function (params) {
                    return {
                        limit: params.limit,
                        offset: params.offset,
                        name: $('#searchName').val(),
                        sort: 'create_on',
                        order: 'desc',
                        operation: $("#searchOperation").val(),
                        username: $("#searchUsername").val()
                    };
                },
                // //请求服务器数据时,你可以通过重写参数的方式添加一些额外的参数,例如 toolbar 中的参数 如果
                // queryParamsType = 'limit' ,返回参数必须包含
                // limit, offset, search, sort, order 否则, 需要包含:
                // pageSize, pageNumber, searchText, sortName,
                // sortOrder.
                // 返回false将会终止请求
                columns: [
                    {
                        checkbox: true
                    },
                    {
                        field: 'id', // 列字段名
                        title: '序号' // 列标题
                    },
                    {
                        field: 'userId',
                        title: '用户Id'
                    },
                    {
                        field: 'username',
                        title: '用户名'
                    },
                    {
                        field: 'operation',
                        title: '操作'
                    },
                    {
                        field: 'time',
                        title: '用时'
                    },
                    {
                        field: 'method',
                        title: '方法'
                    },
                    {
                        field: 'params',
                        title: '参数'
                    },
                    {
                        field: 'ip',
                        title: 'IP地址'
                    },
                    {
                        field: 'createOn',
                        title: '创建时间'
                    },
                    {
                        title: '操作',
                        field: 'id',
                        align: 'center',
                        formatter: function (value, row, index) {
                            var e = '<a class="btn btn-primary btn-sm" href="#" mce_href="#" title="编辑" οnclick="edit(\''
                                + row.userId
                                + '\')"><i class="fa fa-edit"></i></a> ';
                            var d = '<a class="btn btn-warning btn-sm" href="#" title="删除"  mce_href="#" οnclick="remove(\''
                                + row.id
                                + '\')"><i class="fa fa-remove"></i></a> ';
                            var f = '<a class="btn btn-success btn-sm" href="#" title="重置密码"  mce_href="#" οnclick="resetPwd(\''
                                + row.userId
                                + '\')"><i class="fa fa-key"></i></a> ';
                            return d;
                        }
                    }]
            });
}

function reLoad() {
    $('#exampleTable').bootstrapTable('refresh');
}

function remove(id) {
    layer.confirm('确定要删除选中的记录?', {
        btn: ['确定', '取消']
    }, function () {
        $.ajax({
            url: prefix + "/remove",
            type: "post",
            data: {
                'id': id
            },
            beforeSend: function (request) {
                index = layer.load();
            },
            success: function (r) {
                if (r.code == 0) {
                    layer.close(index);
                    layer.msg(r.msg);
                    reLoad();
                } else {
                    layer.msg(r.msg);
                }
            }
        });
    })
}

function batchRemove() {
    var rows = $('#exampleTable').bootstrapTable('getSelections'); // 返回所有选择的行,当没有选择的记录时,返回一个空数组
    if (rows.length == 0) {
        layer.msg("请选择要删除的数据");
        return;
    }
    layer.confirm("确认要删除选中的'" + rows.length + "'条数据吗?", {
        btn: ['确定', '取消']
        // 按钮
    }, function () {
        var ids = new Array();
        // 遍历所有选择的行数据,取每条数据对应的ID
        $.each(rows, function (i, row) {
            ids[i] = row['id'];
        });
        $.ajax({
            type: 'POST',
            data: {
                "ids": ids
            },
            url: prefix + '/batchRemove',
            success: function (r) {
                if (r.code == 0) {
                    layer.msg(r.msg);
                    reLoad();
                } else {
                    layer.msg(r.msg);
                }
            }
        });
    }, function () {
    });
}

日志表映射文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.yangzc.studentboot.common.dao.LogDao">

	<select id="get" resultType="com.yangzc.studentboot.common.domain.LogDO">
		select `id`,`user_id`,`username`,`operation`,`time`,`method`,`params`,`ip`,`create_on` from sys_log where id = #{value}
	</select>

	<select id="list" resultType="com.yangzc.studentboot.common.domain.LogDO">
		select `id`,`user_id`,`username`,`operation`,`time`,`method`,`params`,`ip`,`create_on` from sys_log
        <where>  
		  		  <if test="id != null and id != ''"> and id = #{id} </if>
		  		  <if test="userId != null and userId != ''"> and user_id = #{userId} </if>
		  		  <if test="username != null and username != ''"> and username = #{username} </if>
		  		  <if test="operation != null and operation != ''"> and operation = #{operation} </if>
		  		  <if test="time != null and time != ''"> and time = #{time} </if>
		  		  <if test="method != null and method != ''"> and method = #{method} </if>
		  		  <if test="params != null and params != ''"> and params = #{params} </if>
		  		  <if test="ip != null and ip != ''"> and ip = #{ip} </if>
		  		  <if test="createOn != null and createOn != ''"> and create_on = #{createOn} </if>
		  		</where>
        <choose>
            <when test="sort != null and sort.trim() != ''">
                order by ${sort} ${order}
            </when>
			<otherwise>
                order by id desc
			</otherwise>
        </choose>
		<if test="offset != null and limit != null">
			limit #{offset}, #{limit}
		</if>
	</select>
	
 	<select id="count" resultType="int">
		select count(*) from sys_log
		 <where>  
		  		  <if test="id != null and id != ''"> and id = #{id} </if>
		  		  <if test="userId != null and userId != ''"> and user_id = #{userId} </if>
		  		  <if test="username != null and username != ''"> and username = #{username} </if>
		  		  <if test="operation != null and operation != ''"> and operation = #{operation} </if>
		  		  <if test="time != null and time != ''"> and time = #{time} </if>
		  		  <if test="method != null and method != ''"> and method = #{method} </if>
		  		  <if test="params != null and params != ''"> and params = #{params} </if>
		  		  <if test="ip != null and ip != ''"> and ip = #{ip} </if>
		  		  <if test="createOn != null and createOn != ''"> and create_on = #{createOn} </if>
		  		</where>
	</select>
	 
	<insert id="save" parameterType="com.yangzc.studentboot.common.domain.LogDO" useGeneratedKeys="true" keyProperty="id">
		insert into sys_log
		(
			`user_id`, 
			`username`, 
			`operation`, 
			`time`, 
			`method`, 
			`params`, 
			`ip`, 
			`create_on`
		)
		values
		(
			#{userId}, 
			#{username}, 
			#{operation}, 
			#{time}, 
			#{method}, 
			#{params}, 
			#{ip}, 
			#{createOn}
		)
	</insert>
	 
	<update id="update" parameterType="com.yangzc.studentboot.common.domain.LogDO">
		update sys_log 
		<set>
			<if test="userId != null">`user_id` = #{userId}, </if>
			<if test="username != null">`username` = #{username}, </if>
			<if test="operation != null">`operation` = #{operation}, </if>
			<if test="time != null">`time` = #{time}, </if>
			<if test="method != null">`method` = #{method}, </if>
			<if test="params != null">`params` = #{params}, </if>
			<if test="ip != null">`ip` = #{ip}, </if>
			<if test="creatOn != null">`create_on` = #{createOn}</if>
		</set>
		where id = #{id}
	</update>
	
	<delete id="remove">
		delete from sys_log where id = #{value}
	</delete>
	
	<delete id="batchRemove">
		delete from sys_log where id in 
		<foreach item="id" collection="array" open="(" separator="," close=")">
			#{id}
		</foreach>
	</delete>

</mapper>

github项目地址

https://github.com/yangzc23/studentboot

参考资料

[01] AOP拦截日志报错
[02] Springboot中Aspect实现切面(以记录日志为例)
[03] Spring Aspect的Execution表达式
[04] 再也不怕aop的原理了
[05] table表格的宽度设置,及Bootstrap表格宽度不生效的解决方法
[06] bootstrap table设置列宽
[07] Bootstrap Table列宽拖动的方法

微信扫一扫关注公众号
image.png
点击链接加入群聊

https://jq.qq.com/?_wv=1027&k=5eVEhfN
软件测试学习交流QQ群号:511619105

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值