[toc]

Requirements describe

  1. There are many interfaces in the project. Now we need to implement a function to collect statistics of the interface calls. The main function is as follows:
    • Requirement 1: Record the call times of each interface every day;
    • Requirement 2: If a call throws an exception, log the exception.
    • Requirement 3: Traffic limiting, which limits the number of calls to an interface by a single IP address in a day.

The profile design

  1. Since statistics are needed for each interface invocation, AOP was chosen to implement, abstracting the Controller layer as a facet

    • @Before Determines traffic limiting Before performing service operations.

    • AfterReturn increments the number of calls if it returns normally;

    • @afterThrowing Records the exception if it is thrown.

    If such information is written to the database, it will bring extra overhead for each interface to operate the database and affect the interface response time. Moreover, there are many such recorded information, so Redis is selected here to cache the information.

  2. Redis design

    • For requirement 1, we need to record three pieces of information: 1. 2. Call date (accurate to day); 3. Number of calls. Therefore, the key of Redis uses the Hash structure, and the data structure is as follows: key = interface URI, key = call date (up to day), value = call times (the initial value is 1 and increases by 1 after no call).
    • For requirement 2, the following information needs to be recorded: 1. 2, the occurrence time of the exception (accurate to milliseconds); 3. Exception information. Because the key of requirement 1 has been set as the interface URI, the URI + _exception suffix is used to represent the key of exception information. Therefore, the data structure of Redis is designed as follows (still using Hash structure) : key = URI + “_exception”, key = exception occurrence time (accurate to millisecond), value = exception information.
    • For requirement 3, we need to record the following information: 1. 2. IP address; 3. Call time; 4. Number of calls. This requirement requires a lot of information to be recorded, but we can combine information 1, 2, and 3 to form a unique key. The dimension of call time is accurate to days and the expiration time of the key is set to a day. Such a key represents the interfaces accessed by a single IP address in a day. Therefore, the data structure of Redis is designed as follows: key = URI + IP +date (accurate to day), value = number of calls.

Code implementation

/** * Interface call monitoring * 1. Monitor the number of calls on a single interface in a day * 2. If an exception is thrown, record the exception information and occurrence time * 3@author csh
 * @date2019/10/30 * /
@Aspect
@Component
public class ApiCallAdvice {

    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static final String FORMAT_PATTERN_DAY = "yyyy-MM-dd";
    private static final String FORMAT_PATTERN_MILLS = "yyyy-MM-dd HH:mm:ss:SSS";

    /** * Verify traffic limiting before performing service operations. * The limits are as follows: Number of accesses to a single IP address within a day * key = URI + IP + date (accurate to day) * value = Number of calls */
    @Before("execution(* com.pagoda.erp.platform.controller.*.*(..) )")
    public void before(a) {
        // The request is received, and the request content is recorded
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // Get the requested request
        HttpServletRequest request = attributes.getRequest();

        String uri = request.getRequestURI();
        String date = dateFormat(FORMAT_PATTERN_DAY);
        String ip = getRequestIp(request);

        if (StringUtils.isEmpty(ip)) {
            throw new BusinessException("IP cannot be empty.");
        }
        // URI+IP+ date form a day-based key
        String ipKey = uri + "_" + ip + "_" + date;
        if (redisTemplate.hasKey(ipKey)) {
            if (Integer.parseInt(redisTemplate.opsForValue().get(ipKey).toString()) > 10000) {
                throw new BusinessException("Access failed. Number of accesses exceeded.");
            }
            redisTemplate.opsForValue().increment(ipKey, 1);
        } else {
            stringRedisTemplate.opsForValue().set(ipKey, "1".1L, TimeUnit.DAYS); }}/** * If a result is returned, which represents a call, the number of calls of the corresponding interface is plus one. The statistical dimension is day * (Redis uses Hash structure) * key = URI * key = date (accurate to day) * value = number of calls */
    @AfterReturning("execution(* com.pagoda.erp.platform.controller.*.*(..) )")
    public void afterReturning(a) {
        // The request is received, and the request content is recorded
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // Get the requested request
        HttpServletRequest request = attributes.getRequest();

        String uri = request.getRequestURI();
        String date = dateFormat(FORMAT_PATTERN_DAY);
        if (redisTemplate.hasKey(uri)) {
            redisTemplate.boundHashOps(uri).increment(date, 1);
        } else {
            redisTemplate.boundHashOps(uri).put(date, 1); }}/** * If the call throws an exception, cache exception information (Redis uses a Hash structure) * key = URI + "_exception" * key = time (to the millisecond) * value = exception exception information **@paramEx Exception information */
    @AfterThrowing(value = "execution(* com.pagoda.erp.platform.controller.*.*(..) )", throwing = "ex")
    public void afterThrowing(Exception ex) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        String uri = request.getRequestURI() + "_exception";
        String time = dateFormat(FORMAT_PATTERN_MILLS);
        String exception = ex.getMessage();

        redisTemplate.boundHashOps(uri).put(time, exception);
    }

    private String getRequestIp(HttpServletRequest request) {
        // Get the requested IP address
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
            ip = "" + request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
            ip = "" + request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
            ip = "" + request.getRemoteAddr();
        }
        return ip;
    }

    private String dateFormat(String pattern) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(pattern);
        return dateFormat.format(newDate()); }}Copy the code

The resources

  • An AOP + Redis + annotation implementation limits the number of times an interface can be accessed per unit of time
  • Spring AOP instance