您现在的位置是:首页 >学无止境 >springboot aop统一获取请求参数网站首页学无止境

springboot aop统一获取请求参数

荔枝hu 2024-06-17 10:43:07
简介springboot aop统一获取请求参数

    近期项目上要求所有用户端请求加上审计日志,记录用户行为。于是把遗忘在角落的AOP又拿了出来。这篇文章主要讲述统一获取服务器请求,并从中获取当前登录用户、ip地址、行为(解析请求API对应的方法)、访问时间、执行时长等等。

AOP简介

    入门spring的小伙伴们估计对AOP都不陌生,这里也只是简单介绍下,AOP(面向切面编程),举个简单例子,下面有两个处理方法A和B(伪代码),其中每个方法中都需要记录用户访问信息,同一个类中封装一个处理方法暂且可行,若每个request中对应的方法都需要加此类处理,那代码维护是个问题!那么此刻你值得拥有AOP!

methodA() {
	recordUserAccessInfo();
	// dosomething...
}

methodB() {
	recordUserAccessInfo();
	// dosomething...
}

想看详细介绍的可以看下细说Spring——AOP详解(AOP概览)

实践

前情提要:获取用户行为需要解析请求API对应的方法,所以本文依赖Knife4j中各个注解定义的完整性及正确性。

框架及其他介绍

  1. springboot(2.4.13)
  2. 用户认证模块使用shiro
  3. 使用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:有三个类在代码中未定义

  1. UserVo:可自行定义,包含userId、userName、status等
  2. DateUtil:可自行常量类框架
  3. 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
}
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。