在工作中有时候会遇到一些业务需要在接口执行之前,提前进行一些数据操作,例如记录一些日志或者对访问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也是需要性能开销的,会增加服务器的压力,每次调用都会执行一次代码。所以一定要注意。