前言:有个开发共享api的需求,需要把调用的api插入到接口调用日志里面。 但代码重复工作量大的时候,现在总不能还跟之前一样那里需要写哪里吧。 所以想到了用自定义注解 ,aop切面的方式拦截处理。

思路:aop切面使用@Around来控制目标方法的执行。 既然是日志表的话,那么需要 获取方法(接口)的参数,方法名,方法执行结果,方法执行时间,调用者的ip

另一篇:springboot 自定义注解 ,实现接口限流(计数器限流)【强行喂饭版】

好了,直接上代码: 一:创建注解

import java.lang.annotation.*;

// 注解的作用目标为方法

@Target(ElementType.METHOD)

// 注解在运行时保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用

@Retention(RetentionPolicy.RUNTIME)

// 指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值

@Documented

public @interface ShareApiLog {

// 定义注解的属性,需要什么属性自己增加

public String key() default "";

}

二、实现aop切面拦截

import 你上面注解的路径.ShareApiLog;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.core.annotation.Order;

import org.springframework.stereotype.Component;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

import java.text.DecimalFormat;

import java.util.HashMap;

// 声明这是一个切面类

@Aspect

// 表明该类是一个组件,将该类交给spring管理。

@Component

// 指定执行顺序,值越小,越先执行。由于这里会涉及到接口限流的注解,

// 所以这里要指定执行顺序 比接口限流 慢执行

@Order(2)

public class ShareApiLogAspect {

// 这个注解作用及普及 见文章后面解析

@Around("@annotation(shareApiLog)")

public Object afterShareApiLogMethod(ProceedingJoinPoint joinPoint, ShareApiLog shareApiLog) throws Throwable {

System.out.println("执行了带有@ShareApiLog注解的方法:" + joinPoint.getSignature().getName());

//获取方法的参数,取决于自己业务需不需要

Object[] args = joinPoint.getArgs();

String appId = null;

if (args != null && args.length >= 1) {

//获取appid

appId = (String) args[0];

}

// 获取接口key,取决于自己业务需不需要

String apiKey = shareApiLog.key();

// 获取调用方法的IP地址

String ipAddress = getClientIp();

//根据IP地址,获取调用方的国家-省份-城市

String ipLocation = getIpLocation(ipAddress);

// 记录方法开始时间

long startTime = System.nanoTime();

// 执行目标方法

HashMap result = (HashMap) joinPoint.proceed();

// 记录方法结束时间

long endTime = System.nanoTime();

// 计算方法运行时间,单位为纳秒

double duration = (endTime - startTime) / 1_000_000_000.0;

DecimalFormat decimalFormat = new DecimalFormat("0.00");

String formattedDuration = decimalFormat.format(duration) + "秒";

System.out.println("---执行时间--==");

System.out.println(formattedDuration);

// 进行你的日志表插入代码与逻辑代码

// 一般建议根据返回code来判断执行结果成功与否

return result;

}

/**

* 获取调用方真实ip [本机调用则得到127.0.0.1]

* 首先尝试从X-Forwarded-For请求头获取IP地址,如果没有找到或者为unknown,则尝试从X-Real-IP请求头获取IP地址,

* 最后再使用request.getRemoteAddr()方法作为备用方案。注意,在多个代理服务器的情况下,

* X-Forwarded-For请求头可能包含多个IP地址,我们取第一个IP地址作为真实客户端的IP地址。

*/

public String getClientIp() {

HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

String ipAddress = request.getHeader("X-Forwarded-For");

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {

ipAddress = request.getHeader("X-Real-IP");

}

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {

ipAddress = request.getRemoteAddr();

}

// 多个代理服务器时,取第一个IP地址

int index = ipAddress.indexOf(",");

if (index != -1) {

ipAddress = ipAddress.substring(0, index);

}

return ipAddress;

}

/**

* 根据给定的IP地址查询该IP地址的地理位置信息(国家、省份、城市)。

* 使用了ip-api.com提供的免费IP地址查询接口。

* @param ipAddress ip地址

* @return 地理位置信息(国家、省份、城市)【内网ip查询不到,即返回 Unknown】

*/

public String getIpLocation(String ipAddress) {

try {

URL url = new URL("http://ip-api.com/json/" + ipAddress);

HttpURLConnection connection = (HttpURLConnection) url.openConnection();

connection.setRequestMethod("GET");

BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));

StringBuilder response = new StringBuilder();

String line;

while ((line = reader.readLine()) != null) {

response.append(line);

}

reader.close();

connection.disconnect();

ObjectMapper objectMapper = new ObjectMapper();

JsonNode responseNode = objectMapper.readTree(response.toString());

String country = responseNode.get("country").asText();

String regionName = responseNode.get("regionName").asText();

String city = responseNode.get("city").asText();

// 拼接成字符串

String locationString = country + "-" + regionName + "-" + city ;

return locationString;

} catch (Exception e) {

e.printStackTrace();

}

return "Unknown";

}

}

三、哪里需要点哪里

@PostMapping("/接口api")

@ShareApiLog(key = "根据自己业务选择是否需要key")

public AjaxResult selectToUserId(参数){}

解析:

@Around("@annotation(shareApiLog)") :

- 使用了@Around注解来表示它是一个环绕通知。在Spring AOP中,环绕通知是最强大的通知类型,

它可以在目标方法执行之前和之后执行自定义的逻辑。

- @annotation(shareApiLog) 中的shareApiLog是指参数名称,而不是注解名称。

- @annotation(shareApiLog) 中的shareApiLog参数类型为ShareApiLog,

表示你将拦截被@ShareApiLog注解标记的方法,并且可以通过这个参数来获取@ShareApiLog注解的信息。

如果你想拦截其他注解,只需将第二个参数的类型修改为对应的注解类型即可。

注意: 环绕通知必须显式调用 joinPoint.proceed() 来继续执行目标方法,

否则目标方法将被阻塞,不会得到执行。

普及:

JoinPoint是Spring AOP中的一个接口,它代表了在程序执行过程中能够被拦截的连接点(Join Point)。

连接点指的是在应用程序中,特定的代码块,比如方法的调用、方法的执行、构造器的调用等。

JoinPoint在AOP中的作用是用于传递方法调用的信息,比如方法名、参数、所属的类等等。

当AOP拦截到一个连接点时,就可以通过JoinPoint对象来获取这些信息,并根据需要进行相应的处理。

在AOP中,常见的通知类型(advice)如下:

@Before:在目标方法执行之前执行。

@After:在目标方法执行之后(无论是否抛出异常)执行。

@AfterReturning:在目标方法成功执行之后执行。

@AfterThrowing:在目标方法抛出异常后执行。

@Around:在目标方法执行前后都执行,可以控制目标方法的执行。

在以上各种通知中,可以使用JoinPoint参数来获取连接点的相关信息。

例如,在@Around通知中,可以使用JoinPoint对象来获取目标方法的信息,

比如方法名、参数等。这样,我们就可以根据这些信息来实现我们需要的切面逻辑。

eg:

// 获取方法名

String methodName = joinPoint.getSignature().getName();

//获取方法参数

Object[] args = joinPoint.getArgs();

// 获取所属类名

String className = joinPoint.getSignature().getDeclaringTypeName();

// 获取源代码位置信息

SourceLocation sourceLocation = joinPoint.getSourceLocation();

文章链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: