本文章介绍采用两种不同方式处理----系统登录、系统退出登录两种场景日志。
package com.fy.test.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import java.io.Serializable;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @ClassName: LoginLogEntity
* @Description:
* @Author fy
* @Date 2023/07/10 9:00
*/
@Data
@Accessors(chain = true)
@TableName("t_login_log")
public class LoginLogEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private String id;
/**
* 操作系统
*/
private String opOs;
/**
* 浏览器类型
*/
private String opBrowser;
/**
* 登录IP地址
*/
private String opIp;
/**
* 登录时间
*/
private LocalDateTime opDate;
/**
* 登录用户ID
*/
private String userId;
/**
* 登录用户名称
*/
private String userName;
/**
* 错误类型
*/
private String exCode;
/**
* 错误信息
*/
private String exMsg;
/**
* 登录状态
*/
private boolean status;
/**
* 描述
*/
private String desc;
}
package com.fy.test.log.annotation;
import java.lang.annotation.*;
/**
* @ClassName: LoginLogAop
* @Description:
* @Author fy
* @Date 2023/07/10 9:05
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginLogAop {
/**
* 描述
*/
String desc() default "";
}
package com.fy.test.log.aspect;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.fy.test.log.annotation.LoginLogAop;
import com.fy.test.utils.RequestHolder;
import com.fy.test.utils.SecurityUtil;
import com.fy.test.service.dto.LoginLogDto;
import com.fy.test.service.feign.LogServiceFeign;
import com.fy.test.service.vo.UserVo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterThrowing;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName: LoginLogAspect
* @Description:
* @Author fy
* @Date 2023/07/10 9:05
*/
@Slf4j
@Aspect
public class LoginLogAspect {
@Autowired
private LogServiceFeign logServiceFeign;
/**
* 配置织入点
*/
@Pointcut("@annotation(com.fy.test.common.log.annotation.LoginLogAop)")
public void logPointCut() {
}
/**
* 通知方法会将目标方法封装起来
* 注意:环绕方式选择ProceedingJoinPoint
* Proceedingjoinpoint 继承了JoinPoint,在JoinPoint的基础上暴露出 proceed(), 这个方法是AOP代理链执行的方法。
* JoinPoint仅能获取相关参数,无法执行连接点。
* 暴露出proceed()这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,这跟切面类型有关),
* 就能控制走代理链还是走自己拦截的其他逻辑。
*
* @param joinPoint 切点
*/
@Around(value = "logPointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
LoginLogDto logDto = getLog();
logDto.setStatus(true);
handleLog(joinPoint, logDto);
return result;
}
/**
* 通知方法会在目标方法抛出异常后执行
*
* @param joinPoint
* @param e
*/
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
LoginLogDto logDto = getLog();
logDto.setExCode(e.getClass().getSimpleName()).setExMsg(e.getMessage());
logDto.setStatus(false);
handleLog(joinPoint, logDto);
}
private LoginLogDto getLog() {
HttpServletRequest request = RequestHolder.getHttpServletRequest();
UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
LoginLogDto loginLog = new LoginLogDto();
loginLog.setOpIp(ServletUtil.getClientIP(request))
.setOpOs(userAgent.getOs().getName())
.setOpBrowser(userAgent.getBrowser().getName())
.setUserId(SecurityUtil.getUserId())
.setUserName(SecurityUtil.getUserName())
.setOpDate(LocalDateTime.now());
return loginLog;
}
protected void handleLog(final JoinPoint joinPoint, LoginLogDto loginLogDto) {
// 获得注解
LoginLogAop logAop = getAnnotationLog(joinPoint);
if (null == logAop) {
return;
}
loginLogDto.setDescription(logAop.description());
Map<String, Object> requestParams = getRequestParams(joinPoint);
if (requestParams.containsKey("userVo")) {
UserVo userVo = JSONObject.parseObject(JSON.toJSONString(requestParams.get("userVo")), UserVo.class);
if (null != userVo && StringUtils.isBlank(loginLogDto.getUserName())) {
loginLogDto.setUserName(userVo.getUsername());
}
}
// 保存数据库
logServiceFeign.saveLoginLog(loginLogDto);
}
/**
* 是否存在注解,如果存在就获取
*/
private LoginLogAop getAnnotationLog(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(LoginLogAop.class);
}
return null;
}
/**
* 获取入参
*/
private Map<String, Object> getRequestParams(JoinPoint joinPoint) {
Map<String, Object> requestParams = new HashMap<>();
// 参数名
String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
// 参数值
Object[] paramValues = joinPoint.getArgs();
for (int i = 0; i < paramNames.length; i++) {
Object value = paramValues[i];
// 如果是文件对象
if (value instanceof MultipartFile) {
MultipartFile file = (MultipartFile) value;
// 获取文件名
value = file.getOriginalFilename();
}
requestParams.put(paramNames[i], value);
}
return requestParams;
}
}
package com.fy.test.log.annotation;
import java.lang.annotation.*;
/**
* @ClassName: LogoutLogAop
* @Description:
* @Author fy
* @Date 2023/07/10 9:10
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogoutLogAop {
/**
* 描述
*/
String desc() default "";
}
package com.fy.test.log.aspect;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.fy.test.log.annotation.LoginLogAop;
import com.fy.test.log.annotation.LogoutLogAop;
import com.fy.test.utils.RequestHolder;
import com.fy.test.utils.SecurityUtil;
import com.fy.test.service.dto.LoginLogDto;
import com.fy.test.service.feign.LogServiceFeign;
import com.fy.test.service.vo.UserVo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName: LogoutLogAspect
* @Description:
* @Author fy
* @Date 2023/07/10 9:10
*/
@Slf4j
@Aspect
public class LogoutLogAspect {
@Autowired
private LogServiceFeign logServiceFeign;
/**
* 配置织入点
*/
@Pointcut("@annotation(com.fy.test.log.annotation.LogoutLogAop)")
public void logPointCut() {
}
/**
* 通知方法会将目标方法封装起来
*
* @param joinPoint 切点
*/
@Before("logPointCut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
try {
System.out.println("==============前置处理开始==============");
LoginLogDto logDto = getLog();
logDto.setStatus(true);
handleLog(joinPoint, logDto);
} catch (Exception e) {
//记录本地异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}", e.getMessage());
}
}
/**
* 通知方法会在目标方法抛出异常后执行
*
* @param joinPoint
* @param e
*/
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
LoginLogDto logDto = getLog();
logDto.setExCode(e.getClass().getSimpleName()).setExMsg(e.getMessage());
logDto.setStatus(false);
handleLog(joinPoint, logDto);
}
private LoginLogDto getLog() {
HttpServletRequest request = RequestHolder.getHttpServletRequest();
UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
LoginLogDto loginLogDto = new LoginLogDto();
loginLogDto.setOpIp(ServletUtil.getClientIP(request))
.setOpOs(userAgent.getOs().getName())
.setOpBrowser(userAgent.getBrowser().getName())
.setUserId(SecurityUtil.getUserId())
.setUserName(SecurityUtil.getUserName())
.setOpDate(LocalDateTime.now());
return loginLogDto;
}
protected void handleLog(final JoinPoint joinPoint, LoginLogDto loginLogDto) {
// 获得注解
LogoutLogAop logAop = getAnnotationLog(joinPoint);
if (null == logAop) {
return;
}
loginLogDto.setDesc(logAop.desc());
// 保存数据库
logServiceFeign.saveLoginLog(loginLogDto);
}
/**
* 是否存在注解,如果存在就获取
*/
private LogoutLogAop getAnnotationLog(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(LogoutLogAop.class);
}
return null;
}
/**
* 获取入参
*/
private Map<String, Object> getRequestParams(JoinPoint joinPoint) {
Map<String, Object> requestParams = new HashMap<>();
// 参数名
String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
// 参数值
Object[] paramValues = joinPoint.getArgs();
for (int i = 0; i < paramNames.length; i++) {
Object value = paramValues[i];
// 如果是文件对象
if (value instanceof MultipartFile) {
MultipartFile file = (MultipartFile) value;
// 获取文件名
value = file.getOriginalFilename();
}
requestParams.put(paramNames[i], value);
}
return requestParams;
}
}
Proceedingjoinpoint 继承了JoinPoint,在JoinPoint的基础上暴露出 proceed(), 这个方法是AOP代理链执行的方法。
JoinPoint仅能获取相关参数,无法执行连接点。暴露出proceed()这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,这跟切面类型有关),就能控制走代理链还是走自己拦截的其他逻辑。
import org.aspectj.lang.reflect.SourceLocation;
public interface JoinPoint {
String toString(); //连接点所在位置的相关信息
String toShortString(); //连接点所在位置的简短相关信息
String toLongString(); //连接点所在位置的全部相关信息
Object getThis(); //返回AOP代理对象,也就是com.sun.proxy.$Proxy18
Object getTarget(); //返回目标对象,一般我们都需要它或者(也就是定义方法的接口或类,为什么会是接口呢?
//这主要是在目标对象本身是动态代理的情况下,例如Mapper。所以返回的是定义方法的对象如
//aoptest.daoimpl.GoodDaoImpl或com.b.base.BaseMapper)
Object[] getArgs(); //返回被通知方法参数列表
Signature getSignature(); //返回当前连接点签名。其getName()方法返回方法的FQN,如void aoptest.dao.GoodDao.delete()
//或com.b.base.BaseMapper.insert(T)(需要注意的是,很多时候我们定义了子类继承父类的时候,
//我们希望拿到基于子类的FQN,无法直接拿到,要依赖于
//AopUtils.getTargetClass(point.getTarget())获取原始代理对象,下面会详细讲解)
SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置
String getKind(); //连接点类型
StaticPart getStaticPart(); //返回连接点静态部分
}
public interface ProceedingJoinPoint extends JoinPoint {
public Object proceed() throws Throwable;
public Object proceed(Object[] args) throws Throwable;
}
JoinPoint.StaticPart:提供访问连接点的静态部分,如被通知方法签名、连接点类型等等。
public interface StaticPart {
Signature getSignature(); //返回当前连接点签名
String getKind(); //连接点类型
int getId(); //唯一标识
String toString(); //连接点所在位置的相关信息
String toShortString(); //连接点所在位置的简短相关信息
String toLongString(); //连接点所在位置的全部相关信息
}