文章目录
- 引言
- 步骤一:创建日志注解
- 步骤二:自定义线程连接池
- 步骤三:创建日志类
- 步骤四:创建日志类Mapper
- 步骤五:创建日志service接口及实现类
- 步骤六:创建日志切面类
- 步骤七:创建获取ip工具类
- 步骤八:创建格式转化工具类
- 步骤九:使用注解
- 步骤十:实现效果
- 结论
引言
在 Java 开发中,AOP(面向切面编程)是一种强大的编程范式,它允许我们在程序运行时动态地横切应用的关注点。本文将介绍如何使用 AOP 实现对 Java 接口的实时监控,以便了解接口方法的执行情况。
步骤一:创建日志注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAnno {
/**
* 日志名称
*/
String description() default "";
/**
* 记录日志的操作类型
*/
String type();
}
步骤二:自定义线程连接池
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author myqxin
*/
public class ThreadPoolUtil {
/**
* 线程缓冲队列
*/
private static BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(100);
/**
* 核心线程数,会一直存活,即使没有任务,线程池也会维护线程的最少数量
*/
private static final int SIZE_CORE_POOL = 5;
/**
* 线程池维护线程的最大数量
*/
private static final int SIZE_MAX_POOL = 10;
/**
* 线程池维护线程所允许的空闲时间
*/
private static final long ALIVE_TIME = 2000;
private static ThreadPoolExecutor pool = new ThreadPoolExecutor(SIZE_CORE_POOL, SIZE_MAX_POOL, ALIVE_TIME, TimeUnit.MILLISECONDS, bqueue, new ThreadPoolExecutor.CallerRunsPolicy());
static {
pool.prestartAllCoreThreads();
}
public static ThreadPoolExecutor getPool() {
return pool;
}
public static void main(String[] args) {
System.out.println(pool.getPoolSize());
}
}
步骤三:创建日志类
package com.myqxin.sx.modules.base.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.Map;
import com.myqxin.sx.common.utils.ObjectUtil;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
*
* </p>
*
* @author myqxin
* @since 2021-08-06
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("sx_log")
@Builder
@AllArgsConstructor//创建全参的构造方法,配合@Builder
@ApiModel(value = "SxLog对象", description = "操作日志实体类")
public class SxLog implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
public SxLog() {
}
@ApiModelProperty(value = "操作人")
private String operateor;
@ApiModelProperty(value = "操作类型")
private String operateType;
@ApiModelProperty(value = "操作日期")
private String operateDate;
@ApiModelProperty(value = "方法操作名称")
private String name;
@ApiModelProperty(value = "请求路径")
private String requestUrl;
@ApiModelProperty(value = "请求类型")
private String requestType;
@ApiModelProperty(value = "请求参数")
private String requestParam;
@ApiModelProperty(value = "请求结果")
private String requestBody;
@ApiModelProperty(value = "ip")
private String ip;
@ApiModelProperty(value = "ip信息")
private String ipInfo;
@ApiModelProperty(value = "花费时间")
private Integer costTime;
/**
* 转换请求参数为Json
*
* @param paramMap
*/
public void setMapToParams(Map<String, String[]> paramMap) {
// TODO 实现map转json字符串
this.requestParam = ObjectUtil.mapToString(paramMap);
}
}
步骤四:创建日志类Mapper
import com.myqxin.sx.modules.base.entity.SxLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author myqxin
* @since 2021-08-06
*/
public interface SxLogMapper extends BaseMapper<SxLog> {
}
步骤五:创建日志service接口及实现类
public interface SxLogService extends IService<SxLog> {
public boolean addLog(SxLog sxLog);
}
@Service
public class SxLogServiceImpl extends ServiceImpl<SxLogMapper, SxLog> implements SxLogService {
@Override
public boolean addLog(SxLog sxLog) {
return super.save(sxLog);
}
}
步骤六:创建日志切面类
package com.myqxin.sx.common.aop;
import com.myqxin.sx.common.annotation.LogAnno;
import com.myqxin.sx.common.utils.IpInfoUtil;
import com.myqxin.sx.common.utils.ThreadPoolUtil;
import com.myqxin.sx.modules.base.entity.SxLog;
import com.myqxin.sx.modules.base.service.mybatis.SxLogService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author: myqxin
* @Desc:
* @create: 2021-08-06 15:11
**/
@Aspect
@Component
@Slf4j
public class LogAopAspect {
private static final ThreadLocal<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal beginTime");
@Autowired
private SxLogService sxLogService;
@Autowired(required = false)
private HttpServletRequest request;
@Autowired
private IpInfoUtil ipInfoUtil;
/**
* Controller层切点,注解方式
*/
//@Pointcut("execution(* *..controller..*Controller*.*(..))")
@Pointcut("@annotation(com.myqxin.sx.common.annotation.LogAnno)")
public void controllerAspect() {
}
/**
* 前置通知 (在方法执行之前返回)用于拦截Controller层记录用户的操作的开始时间
*
* @param joinPoint 切点
* @throws InterruptedException
*/
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) throws InterruptedException {
//线程绑定变量(该数据只有当前请求的线程可见)
Date beginTime = new Date();
beginTimeThreadLocal.set(beginTime);
}
/**
* 后置通知(在方法执行之后返回) 用于拦截Controller层操作
* 此注解能对返回值作处理
* @param joinPoint 切点
*/
// @AfterReturning(value = "controllerAspect()", returning = "result")
// public void after(JoinPoint joinPoint, Object result) {}
/**
* 后置通知(在方法执行之后返回) 用于拦截Controller层操作
*
* @param joinPoint 切点
*/
@After("controllerAspect()")
public void after(JoinPoint joinPoint) {
try {
Object[] objs = joinPoint.getArgs();
if (objs != null && objs.length > 0) {
for (Object object : objs) {
if (object instanceof HttpServletRequest) {
request = (HttpServletRequest) object;
break;
}
}
}
SxLog log = new SxLog();
// 创建日期
LocalDateTime now = LocalDateTime.now();
String format = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss"));
log.setOperateDate(format);
//日志标题
log.setName(getControllerMethodInfo(joinPoint).get("description").toString());
//操作类型
log.setOperateType(getControllerMethodInfo(joinPoint).get("type").toString());
//日志请求url
log.setRequestUrl(request.getRequestURI());
//请求方式
log.setRequestType(request.getMethod());
//请求参数
Map<String, String[]> logParams = request.getParameterMap();
log.setMapToParams(logParams);
//请求用户
log.setOperateor("myqxin");
//请求IP
log.setIp(ipInfoUtil.getIpAddr(request));
//请求开始时间
Date logStartTime = beginTimeThreadLocal.get();
long beginTime = beginTimeThreadLocal.get().getTime();
long endTime = System.currentTimeMillis();
//请求耗时
Long logElapsedTime = endTime - beginTime;
log.setCostTime(logElapsedTime.intValue());
//调用线程保存至ES
ThreadPoolUtil.getPool().execute(new SaveSystemLogThread(log, sxLogService));
} catch (Exception e) {
log.error("AOP后置通知异常", e);
}
}
/**
* 保存日志至数据库
*/
private static class SaveSystemLogThread implements Runnable {
private SxLog sxLog;
private SxLogService sxLogService;
public SaveSystemLogThread(SxLog sxLog, SxLogService logService) {
this.sxLog = sxLog;
this.sxLogService = logService;
}
@Override
public void run() {
sxLogService.addLog(sxLog);
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param joinPoint 切点
* @return 方法描述
* @throws Exception
*/
public static Map<String, Object> getControllerMethodInfo(JoinPoint joinPoint) throws Exception {
Map<String, Object> map = new HashMap<String, Object>(16);
//获取目标类名
String targetName = joinPoint.getTarget().getClass().getName();
//获取方法名
String methodName = joinPoint.getSignature().getName();
//获取相关参数
Object[] arguments = joinPoint.getArgs();
//生成类对象
Class targetClass = Class.forName(targetName);
//获取该类中的方法
Method[] methods = targetClass.getMethods();
String description = "";
String type = null;
for (Method method : methods) {
if (!method.getName().equals(methodName)) {
continue;
}
Class[] clazzs = method.getParameterTypes();
if (clazzs.length != arguments.length) {
//比较方法中参数个数与从切点中获取的参数个数是否相同,原因是方法可以重载哦
continue;
}
description = method.getAnnotation(LogAnno.class).description();
type = method.getAnnotation(LogAnno.class).type();
map.put("description", description);
map.put("type", type);
}
return map;
}
}
步骤七:创建获取ip工具类
package com.myqxin.sx.common.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @author myqxin
*/
@Slf4j
@Component
public class IpInfoUtil {
/**
* 获取客户端IP地址
*
* @param request 请求
* @return
*/
public String getIpAddr(HttpServletRequest request) {
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.getRemoteAddr();
if (ip.equals("127.0.0.1")) {
//根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ip = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ip != null && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
if ("0:0:0:0:0:0:0:1".equals(ip)) {
ip = "127.0.0.1";
}
return ip;
}
}
步骤八:创建格式转化工具类
package com.myqxin.sx.common.utils;
import cn.hutool.core.util.StrUtil;
import com.google.gson.Gson;
import java.util.HashMap;
import java.util.Map;
/**
* @author myqxin
*/
public class ObjectUtil {
public static String mapToString(Map<String, String[]> paramMap){
if (paramMap == null) {
return "";
}
Map<String, Object> params = new HashMap<>(16);
for (Map.Entry<String, String[]> param : paramMap.entrySet()) {
String key = param.getKey();
String paramValue = (param.getValue() != null && param.getValue().length > 0 ? param.getValue()[0] : "");
String obj = StrUtil.endWithIgnoreCase(param.getKey(), "password") ? "你是看不见我的" : paramValue;
params.put(key,obj);
}
return new Gson().toJson(params);
}
public static String mapToStringAll(Map<String, String[]> paramMap){
if (paramMap == null) {
return "";
}
Map<String, Object> params = new HashMap<>(16);
for (Map.Entry<String, String[]> param : paramMap.entrySet()) {
String key = param.getKey();
String paramValue = (param.getValue() != null && param.getValue().length > 0 ? param.getValue()[0] : "");
params.put(key, paramValue);
}
return new Gson().toJson(params);
}
}
步骤九:使用注解
@GetMapping("/register")
@LogAnno(description = "注册接口",type = "注册")
public BaseResult register(@RequestParam int[] arr,@RequestParam int aa) {
System.err.println(arr);
return BaseResult.ok("注册成功");
}
步骤十:实现效果
结论
通过使用 AOP,我们能够轻松实现对接口方法的实时监控,而无需修改接口实现类的代码。这种方式使得我们可以在不影响原有逻辑的情况下,加入一些横切关注点,例如性能监控、日志记录等。 AOP 是一个强大的编程范式,能够提高代码的模块化和可维护性。