当我们在做一个后台管理系统的时候,很多时候都会需要一个专门的日志模块,来记录登录的用户的操作,其一可以便于监测数据变化,其二,也可以记录用户做的一些操作,便于我们追根溯源,其三,当我们系统出现问题的时候,也可以通过查看日志,找出问题出在哪里,比如Tomcat Localhost Log。
我们都知道,Spring是一个轻量级的IOC和AOP容器,那么IOC其实就是Inverse of Control,即控制反转,就是将创建对象的权利交给Spring,由它给我们创建对象,对象默认是单例的;AOP其实就是Aspect Oriented Programming 即基于动态代理实现的面向切面编程,简单理解来说,就比如切西瓜,把西瓜切上两刀,然后在两个切面拼接进你想要加入的东西,然后再连起来。
那么我们有了AOP其实思路就会很明确了,只需要在要执行的目标方法之前和之后,插入我们想要插入的代码,就可以了
需要说明一下,我这里的Getter&Setter方法因为使用的 Lombok
插件,所以直接加上 @Data
由插件底层帮我实现了,这个除了需要在idea里面下载 Lombok
的插件,还需要导入pom依赖,这里把依赖附上
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> <scope>provided</scope> </dependency>
package com.arvin.crm.domain; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.util.Date; //系统日志记录 @Data public class SystemLog extends BaseDomain { //操作用户 应该是用户的姓名 private String opUser; //操作时间 private Date opTime; //登录ip private String opIp; //使用功能 private String function; //操作参数信息 private String params; //操作类型 private String operateType; //操作结果 private String operateResult; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") public Date getOpTime() { return opTime; } @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") public void setOpTime(Date opTime) { this.opTime = opTime; } }
这个地方需要说明一下,我的SystemLogMapper里面什么都没有写,是因为我集成了 BaseMapper
,即将公共的方法抽取到了 BaseMapper
里面去
package com.arvin.crm.mapper; /* *@ClassName:EmployeeMapper *@Author:Arvin_yuan *@Date:2020/3/21 21:19 *@Description:TODO */ import com.arvin.crm.domain.SystemLog; public interface SystemLogMapper extends BaseMapper<SystemLog> { }
这里要说明一下的是,我的高级查询和分页是专门封装了一个BaseQuery和SystemLogQuery,因为感觉好像跟我们的主题没有大的关系,所以就没有贴代码上来,不过有需要的话,可以留言或者私信我,我私发或者改贴都可以
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE mapper PUBLIC "-//batis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.arvin.crm.mapper.SystemLogMapper"> <!--抽取的公共的查询所有的代码--> <sql id="All"> select s.id, s.opUser, s.opTime, s.opIp, s.function, s.params, s.operatetype, s.operateresult, t.companyName tcompanyName from t_systemLog s </sql> <!--查询所有--> <select id="getAll" resultType="systemLog"> <include refid="All"/> </select> <!--增加--> <insert id="save" useGeneratedKeys="true" keyProperty="id" parameterType="systemLog"> insert into t_systemLog( opUser, opTime, opIp, function, params, operatetype, operateresult ) values( #{opUser}, #{opTime}, #{opIp}, #{function}, #{params}, #{operatetype}, #{operateresult} </insert> <!--删除--> <delete id="delete" parameterType="Long"> delete from t_systemLog where id = #{id} </delete> <!--查询所有的条数--> <sql id="count"> select count(*) from t_systemLog </sql> <!--高级查询--> <sql id="sqlWhere"> <where> <if test="opUser != null and opUser != ''"> and opUser like concat("%",#{opUser},"%") </if> <if test="startTime != null and endTime != null"> and opTime >= #{startTime} and opTime <= #{endTime} </if> <if test="opIp != null and opIp != ''"> and opIp = #{opIp} </if> <if test="function != null and function != ''"> and function like concat("%",#{function},"%") </if> <if test="params != null and params != ''"> and params like concat("%",#{params},"%") </if> <if test="operatetype != null and operatetype != ''"> and operatetype like concat("%",#{operatetype},"%") </if> <if test="operateresult != null and operateresult != ''"> and operateresult like concat("%",#{operateresult},"%") </if> </where> </sql> <!--查询总条数--> <select id="queryTotal" parameterType="systemLogQuery" resultType="Long"> <include refid="count"/> <include refid="sqlWhere"/> </select> <!--分页查询--> <select id="queryData" parameterType="systemLogQuery" resultMap="queryAllLog"> <include refid="All"/> <include refid="sqlWhere"/> limit ${start}, ${pageSize} </select> <!--封装结果集--> <resultMap id="queryAllLog" type="systemLog"> <id column="id" property="id"></id> <result column="opUser" property="opUser"></result> <result column="opTime" property="opTime"></result> <result column="opIp" property="opIp"></result> <result column="function" property="function"></result> <result column="params" property="params"></result> <result column="operatetype" property="operatetype"></result> <result column="operateresult" property="operateresult"></result> </resultMap> </mapper>
package com.arvin.crm.service; /* *@ClassName:EmployeeService *@Author:Arvin_yuan *@Date:2020/3/21 21:31 *@Description:TODO */ import com.arvin.crm.domain.SystemLog; import java.sql.SQLException; public interface ISystemLogService extends BaseService<SystemLog>{ }
package com.arvin.crm.service.impl; /* *@ClassName:SystemLogServiceImpl *@Author:Arvin_yuan *@Date:2020/3/31 2:33 *@Description:TODO */ import com.arvin.crm.domain.SystemLog; import com.arvin.crm.service.ISystemLogService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class SystemLogServiceImpl extends BaseServiceImpl<SystemLog> implements ISystemLogService { }
哈哈哈,看完是不是一脸懵逼,我这里面上面都没写,算了吧算了吧,还是把抽取的Base代码贴出来哈,上面的BaseQuery先不贴,哎,没贴是怕太长了,看起来不大美观,有需要找我哈
package com.arvin.crm.service; /* *@ClassName:BaseService *@Author:Arvin_yuan *@Date:2020/3/21 20:28 *@Description:TODO */ import com.arvin.crm.query.BaseQuery; import com.arvin.crm.utils.PageList; import java.io.Serializable; import java.util.List; public interface IBaseService<T> { //查找所有 List<T> getAll(); //查找单个 T getOne(Serializable id); //保存 void save(T t); //删除 void delete(Serializable ids); //修改 void update(T t); //分页方法 PageList queryPage(BaseQuery baseQuery); }
package com.arvin.crm.service.impl; /* *@ClassName:BaseServiceImpl *@Author:Arvin_yuan *@Date:2020/3/21 20:28 *@Description:TODO */ import com.arvin.crm.mapper.BaseMapper; import com.arvin.crm.query.BaseQuery; import com.arvin.crm.service.BaseService; import com.arvin.crm.utils.PageList; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.io.Serializable; import java.util.List; @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) public class BaseServiceImpl<T> implements BaseService<T> { @Autowired private BaseMapper<T> baseMapper; @Override public List getAll() { return baseMapper.getAll(); } @Override public T getOne(Serializable id) { return baseMapper.getOne(id); } @Override @Transactional public void save(T t) { baseMapper.save(t); } @Override @Transactional public void delete(Serializable id) { baseMapper.delete(id); } @Override @Transactional public void update(T t) { baseMapper.update(t); } //分页公共的方法 @Override public PageList queryPage(BaseQuery baseQuery) { PageList pageList = new PageList(); //总数 select count(*) from xxx where ? Long total = baseMapper.queryTotal(baseQuery); //select * from xxx where xxx limit List rows = baseMapper.queryData(baseQuery); pageList.setTotal(total); pageList.setRows(rows); return pageList; } }
这个地方有必要说明一下,我只是写了查看、删除和删除多条日志,是因为吧,日志肯定不能做修改,那不然就没有真实性了,但你如果要直接修改数据库,那当我没说,哈哈哈
package com.arvin.crm.web.controller; /* *@ClassName:controller *@Author:Arvin_yuan *@Date:2020/3/21 21:43 *@Description:TODO */ import com.arvin.crm.aspect.SystemLogAnno; import com.arvin.crm.domain.SystemLog; import com.arvin.crm.query.SystemLogQuery; import com.arvin.crm.service.ISystemLogService; import com.arvin.crm.utils.AjaxResult; import com.arvin.crm.utils.PageList; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import java.util.List; @Controller @RequestMapping("/systemLog") @CrossOrigin//前后端分离支持注解 public class SystemLogController { @Autowired private ISystemLogService systemLogService; @SystemLogAnno(operateType = "日志查看") @ApiOperation(value = "查询所有日志", notes = "不需传入参数") @RequestMapping(value = "/page", method = RequestMethod.PATCH) @ResponseBody public PageList queryPage(@RequestBody SystemLogQuery systemLogQuery){ System.out.println(systemLogQuery); PageList pageList = systemLogService.queryPage(systemLogQuery); for (Object row : pageList.getRows()) { //Thu Mar 26 12:08:16 CST 2020 //Tue Apr 14 00:00:00 CST 2020 System.out.println(row); } return systemLogService.queryPage(systemLogQuery); } @SystemLogAnno(operateType = "日志删除") @ApiOperation(value = "删除日志", notes = "传入日志id") @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) @ResponseBody public AjaxResult delete(@PathVariable("id") Long id){ try { systemLogService.delete(id); return new AjaxResult(); } catch (Exception e) { e.printStackTrace(); return new AjaxResult("网络错误,请重试"); } } /** * 批量删除 * @param ids * @return */ @SystemLogAnno(operateType = "删除多条日志") @ApiOperation(value = "批量删除日志", notes = "传入日志id数组") @RequestMapping(value = "/d/{ids}", method = RequestMethod.DELETE) @ResponseBody public AjaxResult deleteList(@PathVariable("ids") Long[] ids){ try { for (Long id : ids) { systemLogService.delete(id); } return new AjaxResult(); } catch (Exception e) { e.printStackTrace(); return new AjaxResult("网络错误,请重试"); } } }
这里的话,这个注解里面我只是写了一句 String operateType() default "";
,这个根据自己需要来写哈,你要写其他的内容,你高兴就好
package com.arvin.crm.aspect; /* *@ClassName:SystemLogAnno *@Author:Arvin_yuan *@Date:2020/3/31 18:50 *@Description:TODO */ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.PARAMETER}) // 方法 + 参数 注解 @Retention(RetentionPolicy.RUNTIME) // 运行时可见 public @interface SystemLogAnno { String operateType() default "";// 记录日志的操作类型 }
这里需要来一条分割线,为什么呢?说实在话,因为下面的内容才是正儿八经的实现类,不,是实现功能的类哈,其他类都是辅助类
SystemLogAspect
这个里面呢,我把我知道的都写在注释上面的,应该大致都能看懂
关于 获取IP
的方法,写了3个,这个我是真不会啊,在网上找的,但是只有方法3我是成功了的,反正嘛,各人情况不同,也许我这行不通说不定哪位朋友拿过去就能执行呢?
我这里获取到的是 IPv4地址
,如果需要IPv6或者其他地址,可以把 inetAddress.getHostAddress();
这个结果打印出来,然后自己找就行,亲测可行,至于这个方法的注释,我是真写不出来,各位看官见谅
package com.arvin.crm.aspect; /* *@ClassName:LogAopAspect *@Author:Arvin_yuan *@Date:2020/3/31 18:53 *@Description:TODO */ import com.arvin.crm.domain.Employee; import com.arvin.crm.domain.SystemLog; import com.arvin.crm.service.ISystemLogService; import com.sun.deploy.net.HttpRequest; import com.sun.xml.internal.bind.CycleRecoverable; import javafx.scene.control.ContextMenuBuilder; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.omg.CORBA.Context; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.sql.SQLException; import java.util.Date; import java.util.Enumeration; @Component @Aspect public class SystemLogAopAspect { @Autowired private ISystemLogService systemLogService;// 系统日志的Service @Autowired private HttpServletRequest request; @Around("@annotation(com.arvin.crm.aspect.LogAnno)")//这个是配置我们实现切面所需注解所在的类 public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { //准备一个结果集对象 Object result = null; // 1.方法执行前的处理,相当于前置通知 // 获取方法签名 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); // 获取方法对象 Method method = methodSignature.getMethod(); // 获取方法名 String methodName = method.getName(); // 获取方法上面的注解 SystemLogAnno anno = method.getAnnotation(SystemLogAnno.class); // 传入方法的参数 String parameters = method.getParameters().toString(); // 获取注解里面描述的类型 对应 String operateType() default ""; String operateType = anno.operateType(); // 获取session中存的当前登录用户信息 Employee employee = (Employee) request.getSession().getAttribute("USER_INFO"); // 创建一个切面对象来调用查询ip的方法 SystemLogAopAspect aop = new SystemLogAopAspect(); String ip = aop.getIpAddress(); // 设置参数进日志对象 // 创建一个日志对象(准备记录日志) SystemLog systemLog = new SystemLog(); // 设置操作用户名 systemLog.setOpUser(employee.getUsername()); // 设置当前用户对应的租户 // systemLog.setTenant(employee.getTenant()); // 设置IP systemLog.setOpIp(ip); // 使用功能 systemLog.setFunction(methodName); // 设置方法参数 systemLog.setParams(parameters); // 操作类型 systemLog.setOperatetype(operateType); try { //这里执行注解对应的方法 result = joinPoint.proceed(); // 2.相当于后置通知(方法成功执行) systemLog.setOperateresult("正常");// 设置操作结果 } catch (SQLException e) { // 3.相当于异常通知部分 systemLog.setOperateresult("失败");// 设置操作结果 } finally { // 4.相当于最终通知 try { // 设置操作时间 systemLog.setOpTime(new Date()); systemLogService.addLog(systemLog);// 添加日志记录 } catch (SQLException e) { e.printStackTrace(); } } return result; } //获取IP 方法一: /*public String getIpAddress() { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; }*/ // 获取IP 方法二: /*public String getIpAddress() { String ip = request.getHeader("X-Real-IP"); if (!StringUtils.isEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { return ip; } ip = request.getHeader("X-Forwarded-For"); if (!StringUtils.isEmpty(ip) && "unknown".equalsIgnoreCase(ip)) { int index = ip.indexOf(","); if (index != -1) { return ip.substring(0, index); } else { return ip; } } else { return request.getRemoteAddr(); } }*/ //获取IP 方法三 public String getIpAddress() throws SocketException { Enumeration e = NetworkInterface.getNetworkInterfaces(); String ip = null; while (e.hasMoreElements()) { NetworkInterface network = (NetworkInterface) e.nextElement(); Enumeration enumeration = network.getInetAddresses(); while (enumeration.hasMoreElements()) { InetAddress inetAddress = (InetAddress) enumeration.nextElement(); String hostAddress = inetAddress.getHostAddress(); if (hostAddress.length() == 11){ ip = hostAddress; } } } return ip; } }
这里的东西是死的,就不多说了
<!--aop配置--> <!--开启spring对aop的注解支持--> <aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true"></aop:aspectj-autoproxy> <!--开启对类的强制代理--> <aop:config proxy-target-class="true"></aop:config> <!--扫描的包,下面是包含Controller,--> <context:component-scan base-package="com.arvin.crm.service"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <aop:config> <!--定义切点--> <aop:pointcut id="pointcut" expression="execution(* com.arvin.crm.web.controller.*Controller.*(..))"/> <!--配置切面类--> <aop:aspect ref="aopAspect"> <!--定义切点,切的方法,和要切谁--> <aop:around method="aroundAdvice" pointcut-ref="pointcut"/> </aop:aspect> </aop:config> <!--配置切面的类--> <bean id="aopAspect" class="com.arvin.crm.aspect.SystemLogAopAspect"/>
需要在Springmvc的配置文件中进行配置
applicationContext-mvc.xml
<!-- 自动扫描该包,使SpringMVC认为包下用了@controller注解的类是控制器 --> <context:component-scan base-package="com.arvin.crm.web.controller" /> <aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true"></aop:aspectj-autoproxy> <aop:config proxy-target-class="true"></aop:config>
到这里基本就结束了,但是我配完了发现有一个问题,日志量非常大啊,很占空间,所以准备加个定时器定期清理,说实话早忘了,好在我写了blog,哈哈哈,在这里 https://segmentfault.com/a/11...