问题分析
当我们在做一个后台管理系统的时候,很多时候都会需要一个专门的日志模块,来记录登录的用户的操作,其一可以便于监测数据变化,其二,也可以记录用户做的一些操作,便于我们追根溯源,其三,当我们系统出现问题的时候,也可以通过查看日志,找出问题出在哪里,比如Tomcat Localhost Log。
解决思路
我们都知道,Spring是一个轻量级的IOC和AOP容器,那么IOC其实就是Inverse of Control,即控制反转,就是将创建对象的权利交给Spring,由它给我们创建对象,对象默认是单例的;AOP其实就是Aspect Oriented Programming 即基于动态代理实现的面向切面编程,简单理解来说,就比如切西瓜,把西瓜切上两刀,然后在两个切面拼接进你想要加入的东西,然后再连起来。
那么我们有了AOP其实思路就会很明确了,只需要在要执行的目标方法之前和之后,插入我们想要插入的代码,就可以了
具体实现
1.日志Domain
需要说明一下,我这里的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>
具体Domain如下
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;
}
}
2.Mapper层
2.1 SystemLogMapper
这个地方需要说明一下,我的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> {
}
2.2 SystemLogMapper.xml
这里要说明一下的是,我的高级查询和分页是专门封装了一个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>
3.SystemLogService
3.1 ISystemLogService
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>{
}
3.2 SystemLogServiceImpl
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先不贴,哎,没贴是怕太长了,看起来不大美观,有需要找我哈
IBaseService
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);
}
BaseServiceImpl
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;
}
}
4.SystemLogController
这个地方有必要说明一下,我只是写了查看、删除和删除多条日志,是因为吧,日志肯定不能做修改,那不然就没有真实性了,但你如果要直接修改数据库,那当我没说,哈哈哈
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("网络错误,请重试");
}
}
}
5.自定义注解
这里的话,这个注解里面我只是写了一句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 "";// 记录日志的操作类型
}
这里需要来一条分割线,为什么呢?说实在话,因为下面的内容才是正儿八经的实现类,不,是实现功能的类哈,其他类都是辅助类
6.切面类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;
}
}
7.Spring的applicationContext.xml配置
这里的东西是死的,就不多说了
<!--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"/>
8.补充Controller使用这个注解
需要在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...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。