您现在的位置是:首页 >学无止境 >springboot aop统一获取请求参数网站首页学无止境
springboot aop统一获取请求参数
简介springboot aop统一获取请求参数
近期项目上要求所有用户端请求加上审计日志,记录用户行为。于是把遗忘在角落的AOP又拿了出来。这篇文章主要讲述统一获取服务器请求,并从中获取当前登录用户、ip地址、行为(解析请求API对应的方法)、访问时间、执行时长等等。
AOP简介
入门spring的小伙伴们估计对AOP都不陌生,这里也只是简单介绍下,AOP(面向切面编程)
,举个简单例子,下面有两个处理方法A和B(伪代码),其中每个方法中都需要记录用户访问信息,同一个类中封装一个处理方法暂且可行,若每个request中对应的方法都需要加此类处理,那代码维护是个问题!那么此刻你值得拥有AOP!
methodA() {
recordUserAccessInfo();
// dosomething...
}
methodB() {
recordUserAccessInfo();
// dosomething...
}
想看详细介绍的可以看下细说Spring——AOP详解(AOP概览)。
实践
前情提要:获取用户行为需要解析请求API对应的方法,所以本文依赖Knife4j中各个注解定义的完整性及正确性。
框架及其他介绍
- springboot(2.4.13)
- 用户认证模块使用shiro
- 使用knife4j(3.0.3)进行API文档管理
使用的是starter脚手架包,其中已包含aop模块
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.13</version>
<relativePath />
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<!--在引用时请在maven中央仓库搜索最新版本号 -->
<version>3.0.3</version>
</dependency>
</dependencies>
审计日志实体类
package com.lizzy.aspect;
import java.util.Date;
import com.alibaba.fastjson.annotation.JSONField;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "审计日志新增实体")
public class AuditLogAddInVo {
/**
* 用户id
*/
@ApiModelProperty(value = "用户id")
private Integer userId;
/**
* 用户名称
*/
@ApiModelProperty(value = "用户名称")
private String userName;
/**
* 访问ip
*/
@ApiModelProperty(value = "访问ip")
private String accessIp;
/**
* 访问url
*/
@ApiModelProperty(value = "访问url")
private String accessUrl;
/**
* 请求参数
*/
@ApiModelProperty(value = "请求参数")
private Object requestParam;
/**
* 执行类
*/
@ApiModelProperty(value = "执行类")
private String className;
/**
* 执行方法
*/
@ApiModelProperty(value = "执行方法")
private String method;
/**
* 方法描述
*/
@ApiModelProperty(value = "方法描述")
private String description;
/**
* 执行结果
*/
@ApiModelProperty(value = "执行结果")
private Object response;
/**
* 执行时间
*/
@ApiModelProperty(value = "执行时长(毫秒)")
private Long executeTime;
/**
* 访问时间
*/
@ApiModelProperty(value = "访问时间")
@JSONField(format = "yyyy-MM-dd HH:mm:ss.SSS")
private Date accessDateTime;
/**
* 推送时间
*/
@ApiModelProperty(value = "推送时间")
@JSONField(format = "yyyy-MM-dd HH:mm:ss.SSS")
private Date pushDateTime;
}
日志切面 AuditAspect
NOTE:有三个类在代码中未定义
- UserVo:可自行定义,包含userId、userName、status等
- DateUtil:可自行常量类框架
- IpUtil:可自行常量类框架
package com.lizzy.aspect;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.SecurityUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
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.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.fastjson.JSON;
import com.lizzy.common.util.DateUtil;
import com.lizzy.common.util.IpUtil;
import com.lizzy.shiro.vo.UserVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@Aspect
@Component
@Slf4j
public class AuditAspect {
private ExecutorService executorService = Executors.newSingleThreadExecutor();
/**
* 切入点
*/
@Pointcut("execution(public * com.lizzy.controller..*.*(..))")
public void execute() {
}
/**
* 前置通知
* @param joinPoint
*/
@org.aspectj.lang.annotation.Before(value = "execute()")
public void Before(JoinPoint joinPoint) {
//log.debug("执行方法之前");
}
/**
* 环绕通知
* @return
*/
@Around(value = "execute()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Date accessDate = new Date();
Object obj = point.proceed();
Date processEndDate = new Date();
//获取当前用户信息
UserVo user = (UserVo) SecurityUtils.getSubject().getPrincipal();
//无用户状态也需记录日志,默认为匿名用户
if (null == user) {
user = ANON_USER;
}
saveAuditLog(user, point, accessDate, processEndDate, obj);
return obj;
}
/**
* 保存审计日志
* @param user 当前登录用户
* @param point 切面
* @param accessDate 业务处理开始时间
* @param processEndDate 业务处理完成时间
* @param obj 返回值
*/
private void saveAuditLog(UserVo user, ProceedingJoinPoint point, Date accessDate, Date processEndDate, Object obj) {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
try {
AuditLogAddInVo addInVo = new AuditLogAddInVo();
addInVo.setUserId(user.getUserId());
addInVo.setUserName(user.getUserName());
String userIp = IpUtil.getIpAddr(request);
addInVo.setAccessIp(userIp);
String accessUrl = request.getServletPath();
addInVo.setAccessUrl(accessUrl);
addInVo.setClassName(point.getSignature().getDeclaringTypeName());
addInVo.setMethod(point.getSignature().getName());
//过滤掉部分请求参数,方法参数为HttpServletRequest HttpServletResponse MultipartFile
List<Object> filterList = Arrays.asList(point.getArgs()).stream().filter(new Predicate<Object>() {
@Override
public boolean test(Object obj) {
if (obj instanceof ServletRequest
|| obj instanceof ServletResponse
|| obj instanceof MultipartFile) {
return false;
}
return true;
}
}).collect(Collectors.toList());
addInVo.setRequestParam(filterList.toArray());
//设置日志描述,形如:平台端-用户管理-新增
addInVo.setDescription(getDecription(accessUrl, point));
addInVo.setAccessDateTime(accessDate);
//获取处理的毫秒数
long executeTime = DateUtil.getDiffMillis(processEndDate, accessDate);
addInVo.setExecuteTime(executeTime);
addInVo.setResponse(obj);
executorService.execute(new Runnable() {
@Override
public void run() {
//TODO: 审计日志量大,一般存储至mongodb,此处为了示例改成控制台打印了
log.info("addInVo:{}", JSON.toJSON(addInVo).toString());
}
});
} catch (Throwable e) {
log.error("审计url:" + request.getServletPath() + ",推送日志异常:" + e.getMessage());
}
}
@org.aspectj.lang.annotation.After(value = "execute()")
public void After(JoinPoint point) {
//log.debug("执行方法之后");
}
@AfterReturning(pointcut = "execute()", returning = "obj")
public void AfterReturning(Object obj) {
//log.debug("执行方法之后获取返回值:" + obj);
}
@AfterThrowing(throwing = "e", pointcut = "execute()")
public void doAfterThrowing(Exception e) {
//log.debug("执行方法异常:" + e.getClass().getName());
}
/**
* 组装日志描述,形如:平台端-用户管理-新增
* @param accessUrl 访问url
* @param point 方法切面
* @return
*/
private String getDecription(String accessUrl, ProceedingJoinPoint point) {
StringBuffer sb = new StringBuffer();
//从url中提取platform、shop、subject、doctor
String appTypeDescr = getAppTypeDecription(accessUrl);
if (StringUtils.hasLength(appTypeDescr)) {
sb.append(appTypeDescr);
sb.append(" - ");
}
//controller类@Api注解tags说明
Api api = point.getTarget().getClass().getAnnotation(Api.class);
if (null != api && null != api.tags() && api.tags().length > 0) {
sb.append(StringUtils.arrayToCommaDelimitedString(api.tags()));
sb.append(" - ");
}
//controller类中方法上@ApiOperation注解value值
MethodSignature signature = (MethodSignature) point.getSignature();
// Audit action = signature.getMethod().getAnnotation(Audit.class);
ApiOperation action = signature.getMethod().getAnnotation(ApiOperation.class);
if (null != action && StringUtils.hasLength(action.value())) {
sb.append(action.value());
// sb.append(" - ");
}
return sb.toString();
}
/**
* 截取accessUrl中第一个和第二个/之间的appType
* @param accessUrl 类似于/platform/user/list
* @return
*/
private String getAppTypeDecription(String accessUrl) {
String[] splits = accessUrl.split("/");
if (null == splits || splits.length < 1) {
return "";
}
String appType = splits[1];
String appTypeDescr = "平台端";
if ("user".equalsIgnoreCase(appType)) {
appTypeDescr = "用户端";
}
return appTypeDescr;
}
// 匿名用户(id:-1->匿名用户)
public static final UserVo ANON_USER = new UserVo() {
/**
*
*/
private static final long serialVersionUID = -524406512967805293L;
{
setUserId(-1);
setUserName("匿名用户");
setStatus(-1);
}
};
}
案例controller
@RestController
@RequestMapping("/platform/user")
@Validated
@AllArgsConstructor
@Api(tags = "平台端-用户接口")
@SuppressWarnings("unchecked")
public class UserController {
private final UserService userService;
@ApiOperation("用户详情")
@PostMapping("/one")
public JSONObject one(@RequestParam @ApiParam(value = "请求参数", required = true) Integer id) {
// TODO: 业务代码
}
}
调用该API后,审计日志如下:
addInVo:{
"accessIp":"0:0:0:0:0:0:0:1", //ip获取貌似有点问题
"method":"getDicTypes",
"accessUrl":"/platform/user/one",
"response":{"data":[],"status":0},
"description":"平台端 - 用户接口 - 用户详情",
"className":"com.lizzy.controller.platform.UserController",
"accessDateTime":"2023-05-17 16:02:29.464",
"requestParam":["1"],
"userName":"lizzy",
"userId":1,
"executeTime":29
}
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。