前言:有个开发共享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) {//获取appidappId = (String) args[0];}// 获取接口key,取决于自己业务需不需要String apiKey = shareApiLog.key();// 获取调用方法的IP地址String ipAddress = getClientIp();//根据IP地址,获取调用方的国家-省份-城市String ipLocation = getIpLocation(ipAddress);// 记录方法开始时间long startTime = System.nanoTime();// 执行目标方法HashMap<String, Object> result = (HashMap<String, Object>) 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();