AOP+自定义注解 实现接口限流(Redis方法)

1,445 阅读11分钟

AOP+自定义注解 实现接口限流(Redis方法)

1.自定义注解类:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author zhanglu
* @date Created in 2020/4/9 10:04
* @description 自定义限流注解
* @modifiedBy
* @version: v1
*/
@Retention(RetentionPolicy.RUNTIME)//使用位置(类,方法)
@Target(ElementType.METHOD)//加载到jvm里运行
public @interface AccessLimit {
   int seconds();//redis缓存保存时间
   int maxCount();//最大访问量
   boolean needLogin() default true;//是否登陆
}

1.1 @Retention注解说明

注解@Retention可以用来修饰注解,是注解的注解,称为元注解。
Retention注解有一个属性value,是RetentionPolicy类型的,Enum RetentionPolicy是一个枚举类型,
这个枚举决定了Retention注解应该如何去保持,也可理解为Rentention 搭配 RententionPolicy使用。RetentionPolicy有3个值:CLASS  RUNTIME   SOURCE
按生命周期来划分可分为3类:
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。
那怎么来选择合适的注解生命周期呢?
首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。
一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解,比如@Deprecated使用RUNTIME注解
如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;
如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,使用SOURCE 注解。

注解@Override用在方法上,当我们想重写一个方法时,在方法上加@Override,当我们方法的名字出错时,编译器就会报错
注解@Deprecated,用来表示某个类或属性或方法已经过时,不想别人再用时,在属性和方法上用@Deprecated修饰
注解@SuppressWarnings用来压制程序中出来的警告,比如在没有用泛型或是方法已经过时的时候

1.2 @Target注解说明

1.2.1 @Target的用法

java.lang.annotation.Target
用于设定注解使用范围
java.lang.annotation.ElementType
Target通过ElementType来指定注解可使用范围的枚举集合

1.2.2 ElementType的用法

取值 注解使用范围
METHOD 可用于方法上
TYPE 可用于类或者接口上
ANNOTATION_TYPE 可用于注解类型上(被@interface修饰的类型)
CONSTRUCTOR 可用于构造方法上
FIELD 可用于域上
LOCAL_VARIABLE 可用于局部变量上
PACKAGE 用于记录java文件的package信息
PARAMETER 可用于参数上
这里重点说明下:ElementType.PACKAGE。它并不是使用在一般的类中,而是用在固定的文件package-info.java中。这里需要强调命名一定是“package-info”。由于package-info.java并不是一个合法的类,使用eclipse创建类的方式会提示不合法,所以需要以创建文件的方式来创建package-info.java。

例如,定义可使用范围PACKAGE:
@Target({ElementType.PACKAGE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public@interface  AsynLog {
}
那么,创建文件:package-info.java,内容如下:
@AsynLog
package org.my.commons.logs.annotation; 
重点说明:注解只能在ElementType设定的范围内使用,否则将会编译报错。例如:范围只包含ElementType.METHOD ,则表明该注解只能使用在类的方法上,超出使用范围将编译异常。

2. AOP解析类

import com.idatage.cloud.common.core.util.R;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
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.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;

/**
 * @author zhanglu
 * @date Created in 2020/4/9 10:08
 * @description 限流AOP解析类
 * @modifiedBy
 * @version:
 */

@Aspect//来定义一个切面
@Component
public class AccessLimitAop {


    @Autowired
    private RedisTemplate redisTemplate;
	//定义切入点
    @Pointcut(value = "@annotation(com.learn.AccessLimit)")//AccessLimit注解类的位置
    public void cutLimit() {
    }
    //通知
    @Before("AccessLimit()")
    public void doBefore(JoinPoint joinPoint) {
        System.out.println("触发到 @Before(\"AccessLimit()\")");
    }
    
     /**
     * 后置通知
     *
     * @param joinPoint 切点
     */
    @AfterReturning("AccessLimit()")
    public void doAfrterReturning(JoinPoint joinPoint) {

        Object[] args = joinPoint.getArgs();
        System.out.println("触发 @AfterReturning(\"AccessLimit()\")");
        System.out.println(args.length);
        getControllerMethodDescription(joinPoint);
    }
    
    @Around("cutLimit()")
    public Object recordSysLog(ProceedingJoinPoint point) throws Throwable {
        MethodSignature ms = (MethodSignature) point.getSignature();//获得执行方法的方法名
        Method method = ms.getMethod();//获取公共方法,不包括类私有的
        AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = servletRequestAttributes.getRequest();
        if (null == accessLimit) {
            return point.proceed();
        }
        int seconds = accessLimit.seconds();
        int maxCount = accessLimit.maxCount();
        boolean needLogin = accessLimit.needLogin();
        if (needLogin) {
            //判断是否登录
        }
        String key = getIpAddress(request);
        Object o = redisTemplate.opsForValue().get(key);
        Integer count = Integer.valueOf(o == null ? "-1" : o.toString());
        if (null == count || -1 == count) {
            redisTemplate.opsForValue().increment(key);
            redisTemplate.expire(key, seconds, TimeUnit.SECONDS);
            return point.proceed();
        }
        if (count < maxCount) {
            redisTemplate.opsForValue().increment(key);
            return point.proceed();
        }

        if (count >= maxCount) {
            String str = "请勿频繁刷新";
            return R.failed(str);
        }
        return point.proceed();
    }

    /**
     * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址。
     * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
     * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串
     *
     * @param request
     * @return
     */
    private static String getIpAddress(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.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
            if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
                //根据网卡取本机配置的IP
                InetAddress inet = null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                ip = inet.getHostAddress();
            }
        }
        return ip;
    }
}

3 接口调用

 	/**
     * 获取服务器时间
     */
    @ApiOperation(value = "获取服务器时间", notes = "获取服务器时间")
    @GetMapping("/getcurrenttime")
    @Inner(value = false)
    @AccessLimit(seconds = 60, maxCount = 10, needLogin = false)
    public R getCurrentTime() {
        Long time = System.currentTimeMillis();
        return R.ok(time);
    }

4 Spring Aop(aspect)的详细使用

4.1AOP的基本概念

  • @Aspect: 通常是一个类,里面可以定义切入点和通知

  • JointPoint(连接点): 程序执行过程中明确的点,一般是方法的调用

  • Advice(通知): AOP在特定的切入点上执行的增强处理:

  • @before标识一个前置增强方法,相当于BeforeAdvice的功能

  • @After: final增强,不管是抛出异常或者正常退出都会执行。

  • @AfterReturning: 后置增强,似于AfterReturningAdvice, 方法正常退出时执行

  • @AfterThrowing: 异常抛出增强,相当于ThrowsAdvice

  • @Around: 环绕增强,相当于MethodInterceptor

  • Pointcut(切入点): 带有通知的连接点,在程序中主要体现为书写切入点表达式

  • AOP Proxy:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。

4.2Pointcut配置使用:

表示式(expression)和签名(signature)

//Pointcut表示式
@Pointcut("execution(* com.savage.aop.MessageSender.*(..))")
//Point签名
private void log(){}

由下列方式来定义或者通过 &&、 ||、 !、 的方式进行组合:

execution:用于匹配方法执行的连接点;

within:用于匹配指定类型内的方法执行;

this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;

target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;

args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;

@within:用于匹配所以持有指定注解类型内的方法;

@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;

@args:用于匹配当前执行的方法传入的参数持有指定注解的执行;

@annotation:用于匹配当前执行方法持有指定注解的方法;

格式

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

其中后面跟着“?”的是可选项

括号中各个pattern分别表示:

修饰符匹配(modifier-pattern?)

返回值匹配(ret-type-pattern)可以为*表示任何返回值,全路径的类名等

类路径匹配(declaring-type-pattern?)

方法名匹配(name-pattern)可以指定方法名 或者 代表所有, set 代表以set开头的所有方法

参数匹配((param-pattern))可以指定具体的参数类型,多个参数间用“,”隔开,各个参数也可以用"*" 来表示匹配任意类型的参数,".."表示零个或多个任意参数。

如(String)表示匹配一个String参数的方法;(*,String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型

异常类型匹配(throws-pattern?)

*Pointcut使用详细语法:*

任意公共方法的执行:execution(public * *(..))

任何一个以“set”开始的方法的执行:execution(* set*(..))

AccountService 接口的任意方法的执行:execution(* com.xyz.service.AccountService.*(..))

定义在service包里的任意方法的执行: execution(* com.xyz.service..(..))

定义在service包和所有子包里的任意类的任意方法的执行:execution(* com.xyz.service...(..))

第一个表示匹配任意的方法返回值, ..(两个点)表示零个或多个,第一个..表示service包及其子包,第二个表示所有类, 第三个*表示所有方法,第二个..表示方法的任意参数个数

定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行:execution(* com.test.spring.aop.pointcutexp..JoinPointObjP2.*(..))")

pointcutexp包里的任意类: within(com.test.spring.aop.pointcutexp.*)

pointcutexp包和所有子包里的任意类:within(com.test.spring.aop.pointcutexp..*)

实现了Intf接口的所有类,如果Intf不是接口,限定Intf单个类:this(com.test.spring.aop.pointcutexp.Intf)

当一个实现了接口的类被AOP的时候,用getBean方法必须cast为接口类型,不能为该类的类型

带有@Transactional标注的所有类的任意方法:

@within(org.springframework.transaction.annotation.Transactional) // 亲测成功

@target(org.springframework.transaction.annotation.Transactional) // 亲测未成功

*带有@Transactional标注的任意方法:@annotation(org.springframework.transaction.annotation.Transactional)*

*@within和@target针对类的注解,@annotation是针对方法的注解*

参数带有@Transactional标注的方法:@args(org.springframework.transaction.annotation.Transactional)

参数为String类型(运行是决定)的方法: args(String)

JoinPoint 常用的方法:

Object[] getArgs:返回目标方法的参数

Signature getSignature:返回目标方法的签名

Object getTarget:返回被织入增强处理的目标对象

Object getThis:返回AOP框架为目标对象生成的代理对象

当使用@Around处理时,需要将第一个参数定义为ProceedingJoinPoint类型,该类是JoinPoint的子类。

织入


 @AfterReturning(

pointcut="execution(* com.abc.service.*.access*(..)) && args(time, name)",returning="returnValue")

public void access(Date time, Object returnValue, String name){

​    System.out.println("目标方法中的参数String = " + name);

​    System.out.println("目标方法中的参数Date = " + time);

​    System.out.println("目标方法的返回结果returnValue = " + returnValue);

}

表达式中增加了args(time, name)部分,意味着可以在增强处理的签名方法(access方法)中定义"time"和"name"两个属性。这两个形参的类型可以随意指定,但一旦指定了这两个参数的类型,则这两个形参类型将用于限制该切入点只匹配第一个参数类型为Date,第二个参数类型为String的方法(方法参数个数和类型若有不同均不匹配);access方法只需要满足"time", "name"参数的顺序和pointcut中args(param1, param2)的顺序相同即可,"returnValue"位置顺序无所谓。

//将被access方法匹配

public String accessAdvice(Date d, String n){  

  System.out.println("方法:accessAdvice");

  return"aa";

}

切面执行顺序

同一个方法被多个Aspect类拦截

优先级高的切面类里的增强处理的优先级总是比优先级低的切面类中的增强处理的优先级高。

在“进入”连接点时,最高优先级的增强处理将先被织入(eg.给定的两个不同切面类Before增强处理中,优先级高的那个会先执行);在“退出”连接点时,最高优先级的增强处理会最后被织入(eg.给定的两个不同切面类After增强处理中,优先级高的那个会后执行)。eg.优先级为1的切面类Bean1包含了@Before,优先级为2的切面类Bean2包含了@Around,虽然@Around优先级高于@Before,但由于Bean1的优先级高于Bean2的优先级,因此Bean1中的@Before先被织入。

Spring提供了如下两种解决方案指定不同切面类里的增强处理的优先级:

让切面类实现org.springframework.core.Ordered接口:实现该接口的int getOrder()方法,该方法返回值越小,优先级越高

直接使用@Order注解来修饰一个切面类:使用这个注解时可以配置一个int类型的value属性,该属性值越小,优先级越高

同一个切面类里的两个相同类型的增强处理在同一个连接点被织入时,Spring AOP将以随机的顺序来织入这两个增强处理,没有办法指定它们的织入顺序。即使给这两个 advice 添加了 @Order 这个注解,也不行!

参考文献: my.oschina.net/u/3434392/b…