springboot 自定义注解 ,aop切面@Around; 为接口实现日志插入【强行喂饭版】

前言:有个开发共享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 com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

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.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
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<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();

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北海南风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值