前言:有个开发共享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
// 记录方法结束时间
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();
文章链接
发表评论