引言:
AOP( 面向切面编程)是OOP(面向对象编程)的延续,是软件开发中的一个热点。它所面对的是处理过程中的某个步骤或阶段,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP包括切面(Aspect)、切入点(pointCut)、通知(advice) 、连接点(joinpoint),其中会用到的注解有:
(注:本文中采用注解的方式进行aop操作,没有配置文件。)
1.切面(aspect)
切面类 @Aspect: 定义切面类,在类前面加上@Aspect、@Component注解,标明该类是作为切面。
@Aspect
@Component
public class LimitAspect{
...
}
2.切入点(pointCut)
@Pointcut:Pointcut是植入Advice的触发条件,定义一个切入点表达式,用来确定哪些类需要代理。在Spring 2.0中Pointcut的定义包括2部分,一是表达式(expression),二是方法签名 (signature)。方法签名必须是 public及void型,Pointcut中的方法不需要在方法体内编写实际代码。
//Pointcut表示式
@Pointcut("@annotation(xx.xx.xx.Limit)")
//Point签名
public void pointcut() {}
具体使用格式:
//表示匹配所有方法
@Pointcut("execution(* *(..))")
//表示匹配com.savage.server.UserService中所有的公有方法
@Pointcut("execution(public * com.savage.service.UserService.*(..))")
//表示匹配com.savage.server包及其子包下的所有方法
@Pointcut("execution(* com.savage.server..*.*(..))")
//Limit注解的所有方法
@Pointcut("@annotation(xx.xx.xx.Limit)")
(注:@annotation() 匹配指定注解为切入点的方法,表示标注了某个注解的所有方法。)
3.通知(advice)
通知(advice)包括下面五种:
@Before 在切点方法之前执行
@After 在切点方法之后执行
@AfterReturning 切点方法返回后执行
@AfterThrowing 切点方法抛异常执行
@Around 属于环绕增强,能控制切点执行前,执行后
这里拿@Around举例,可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值; 当需要改变目标方法的返回值时,只能使用Around方法;但是只限线程安全!!!
总而言之:能只用其他四个解决就行,@Around功能强,但是条件也更多。
@Around注解源码:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Around {
String value();
String argNames() default "";
}
注解中可以直接带上方法名,或者写@annotation() 表达式
@Around("pointcut()")
@Around(value = "pointcut()")
@Around(value = "@annotation(around)") //around 与 下面参数名around对应
public void processAuthority(ProceedingJoinPoint point,MyAnnotation around){
...
}
4.连接点(joinpoint)
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象。
接口JoinPoint中封装了代理对象的相关信息:
//获取代理对象
Object getThis();
//获取被代理的对象
Object getTarget();
//获取传入目标方法的参数对象
Object[] getArgs();
//获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Signature getSignature();
5.代码
注解层:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
// 资源名称,用于描述接口功能
String name() default "";
// 资源 key
String key() default "";
// key prefix
String prefix() default "";
// 时间的,单位秒
int period();
// 限制访问次数
int count();
// 限制类型
LimitType limitType() default LimitType.CUSTOMER;
}
切面层:
@Aspect
@Component
public class LimitAspect {
private final RedisTemplate<Object,Object> redisTemplate;
// 用于日志记录切面中信息
private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);
public LimitAspect(RedisTemplate<Object,Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 标注了注解Limit的所有方法
@Pointcut("@annotation(me.zhengjie.annotation.Limit)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// SpringMVC中RequestContextHolder获取请求信息的方法
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method signatureMethod = signature.getMethod();
Limit limit = signatureMethod.getAnnotation(Limit.class);
LimitType limitType = limit.limitType();
String key = limit.key(); // key默认是"",
if (StringUtils.isEmpty(key)) {
if (limitType == LimitType.IP) {
key = StringUtils.getIp(request);
} else {
key = signatureMethod.getName();
}
}
ImmutableList<Object> keys = ImmutableList.of(StringUtils.join(limit.prefix(), "_", key, "_", request.getRequestURI().replaceAll("/","_")));
String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period());
if (null != count && count.intValue() <= limit.count()) {
logger.info("第{}次访问key为 {},描述为 [{}] 的接口", count, keys, limit.name());
return joinPoint.proceed();
} else {
throw new BadRequestException("访问次数受限制");
}
}
/**
* 限流脚本
*/
private String buildLuaScript() {
return "local c" +
"\nc = redis.call('get',KEYS[1])" +
"\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
"\nreturn c;" +
"\nend" +
"\nc = redis.call('incr',KEYS[1])" +
"\nif tonumber(c) == 1 then" +
"\nredis.call('expire',KEYS[1],ARGV[2])" +
"\nend" +
"\nreturn c;";
}
}
注:ProceedingJoinPoint是JoinPoint的子接口,添加了proceed()和proceed(Object[] var1) 两个方法
.
.
.
.
.
参考:
1.@Aspect 注解使用详解
2.Spring详解篇之 AOP面向切面编程
3.Spring注解 AOP@Aspect的详细介绍
4.Spring AOP 中@Pointcut的用法
5.@Around简单使用示例——SpringAOP增强处理