在工作中有时候会遇到一些业务需要在接口执行之前,提前进行一些数据操作,例如记录一些日志或者对访问ip进行限制。传统情况下我们会在业务代码前增加这些日志或者限制,但是这样破话了代码业务的专一性,也不方便阅读。因此可以使用自定义注解和@Aspect注解来处理这种情况。话不多说直接上代码。

首先创建自定义注解,该注解的目的是为了在一定时间段内,限制同一IP地址频繁访问。

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Order(Ordered.HIGHEST_PRECEDENCE)       
public @interface IAccessRestrictionsForSecond {
    
    /**
     * @Description: 限制某时间段内可以访问的次数
     * @return
     */
    int limitCounts() default 100;

    /**
     * @Description: 限制访问的某一个时间段,单位为秒。
     * @return
     */
    int timeSecond() default 60;
}

然后是具体实现

通过使用Map记录用户在区间内的访问情况,大于最大值则禁止访问,由于静态Map变量直接存储在堆栈内存中,使用的必须定时清理,否则长时间使用会导致内存溢出。如果用户不多可以使用Map,如果用户数量很多且活跃度高,建议使用Redis记录用户访问数据。

@Aspect是使用AOP必须的注解,@Pointcut注解是对切入点进行配置,@Before是只允许接口之前执行,@After是指运行接口之后执行,@Around是在接口执行前和执行后都执行一遍代码,因此也成为环绕。@EnableScheduling和@Scheduled是执行定时任务,@Scheduled(cron = "0/60 * * * * ?")的意思是每60秒执行一次。

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.nd.common.util.ObjectUtils;
import com.nd.tsei.service.app.IAccessRestrictionsForSecond;

@Aspect
@Component
@EnableScheduling
public class AccessRestrictionsSecondImpl{
    
    private static final Logger LOG = LoggerFactory.getLogger(AccessRestrictionsSecondImpl.class);

    private static Map<String, Integer> count = new HashMap<String, Integer>();
    
    @Pointcut("execution(* com.nd.tsei.controller.*.*(..)) && @annotation(com.nd.tsei.service.app.IAccessRestrictionsForSecond)")
    public void before(){
    }

    @Before("before()")
    public void requestLimit(JoinPoint joinPoint) throws Exception {
        String userInfo = "";
        try {
            // 获取HttpRequest
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            // 判断request不能为空
            if (request == null) {
                throw new Exception("HttpServletRequest不能为空...");
            }
            
            IAccessRestrictionsForSecond limit = this.getAnnotation(joinPoint);
            if(limit == null) {  
                return;  
            }  
            
            String ip = request.getRemoteAddr();
            String port = String.valueOf(request.getRemotePort());
            String uri = request.getRequestURI().toString();
            String redisKey = "limit-ip-request:" + uri + ":" + ip + ":" + port;
            userInfo = redisKey;
            
            // 如果该key不存在,则从0开始计算,并且当count为1的时候,设置过期时间
            if (ObjectUtils.isEmpty(count.get(redisKey))) {
                count.put(redisKey, 1);
            }else {
                int connCount = count.get(redisKey);
                if (connCount > 0) {
                    //累加1
                    connCount = connCount + 1;
                    count.put(redisKey, Integer.valueOf(connCount));
                }
                // 如果count大于限制的次数,则报错
                if (connCount > limit.limitCounts()) {
                    throw new Exception(limit.timeSecond()+"秒内最多调用"+limit.limitCounts()+"次接口,"+"请勿频繁调用接口。");
                }
            }
        } catch (Exception e) {
            LOG.error(userInfo,e);
            throw e;
        }
    }
    
    /**
     * @Description: 获得注解
     */
    private IAccessRestrictionsForSecond getAnnotation(JoinPoint joinPoint) throws Exception {  
        Signature signature = joinPoint.getSignature();  
        MethodSignature methodSignature = (MethodSignature) signature;  
        Method method = methodSignature.getMethod();  
  
        if (method != null) {  
            return method.getAnnotation(IAccessRestrictionsForSecond.class);  
        }  
        return null;  
    }
    
    @Scheduled(cron = "0/60 * * * * ?")
    private void clearCache(){
        count.clear();
    }
}

然后只要在需要限制的接口上加上注解,就可以进行限制了。

    @IAccessRestrictionsForSecond
    @RequestMapping(value = "/info", method = RequestMethod.GET)
    public JSONObject getInfo() throws Exception {
        return null;
    }

最后需要注意的是,使用AOP也是需要性能开销的,会增加服务器的压力,每次调用都会执行一次代码。所以一定要注意。

posted on 2017-12-01 18:38  超级悠  阅读(661)  评论(0编辑  收藏  举报