项目是用SpringBoot+Vue实现,前后端分离的,前端是用nginx部署的,虽说可以通过Nginx的日志来统计网站的IP的访问次数,但想在前端用图形化的方式来展示是不太可行的,所以我想着是在SpringBoot后端来实时统计访问的网站的IP及其次数和地区,然后存储在数据库中,前端可以通过后端提供的相应的接口获取到数据,接着图形化的渲染访问数据,这里只讲SpringBoot后端这部分。

        这里统计IP的地区用的是ip2region,可以参考我之前写的入门文章

Java中使用ip2region获取IP的地址_像向日葵一样~的博客-CSDN博客_ip2region javaip2region是准确率很高的 ip 地址定位库,本文介绍在Java中使用ip2region获取IP的地址https://blog.csdn.net/zhiwenganyong/article/details/122755057?spm=1001.2014.3001.5502        不过本次我用的是IP库是 ip2region.xdb,是一个更新的库

ip2region: Ip2region (2.0 - xdb) 是一个离线 IP 数据管理框架和定位库,支持亿级别的数据段,10微秒级别的查询性能,提供了许多主流编程语言的 xdb 数据管理引擎的实现。 - Gitee.comhttps://gitee.com/lionsoul/ip2region/tree/master/data         其中累加IP的访问次数是用redis的incr,后端会提供给前端一个接口当访问网站前端会调这个接口,然后用AOP在接口返回加入逻辑(也可以通过拦截器来拦截接口加入逻辑,但得先解决自动注入为null的问题,注入为null是因为拦截器加载是在springcontext创建之前完成的),先捕获请求获取真实IP,接着封装好消息发给MQ,MQ在进行后续操作。

 引入依赖

        Gradle

// https://mvnrepository.com/artifact/org.lionsoul/ip2region
implementation group: 'org.lionsoul', name: 'ip2region', version: '2.6.4'

        Maven

<!-- https://mvnrepository.com/artifact/org.lionsoul/ip2region -->
<dependency>
    <groupId>org.lionsoul</groupId>
    <artifactId>ip2region</artifactId>
    <version>2.6.4</version>
</dependency>

 配置文件

        配置好redis、rabbitmq及IP库的地址

  redis:
    # 数据库索引
    database: 0
    # 单节点redis配置
    host: xxx.xxx.xxx.xxx
    port: 6379
    timeout: 60000
    password: xxxxxx


  # 配置Rabbitmq服务
  rabbitmq:
    host: xxx.xxx.xxx.xxx
    port: 5672
    username: admin
    password: xxx.xxx.xxx.xxx
    publisher-confirm-type: correlated
    listener:
      simple:
        retry:
          # 开启消费者重试机制(默认就是true,false则取消重试机制)
          enabled: true
          # 最大重试次数
          max-attempts: 3
          # 重试间距(单位:秒)
          initial-interval: 2s

  xdb:
    profile: /usr/local/xdb/

 MQ的配置类

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 像向日葵一样~
 * @date 2022-08-03-12:51
 */
@Configuration
public class MQConfig {

    // 定义交换机名称
    public static final String VISIT_COUNT_MSG_EXCHANGE = "visit_count_msg_exchange";
    // 定义队列  系统发给访问统计的消息队列
    public static final String VISIT_HANDLE_QUEUE = "visit_handle_queue";
    // 定义routingKey
    public static final String VISIT_HANDLE_RK = "visit_handle_rk";

    // 声明交换机
    @Bean
    public DirectExchange visitMsgExchange() {
        return ExchangeBuilder.directExchange(VISIT_COUNT_MSG_EXCHANGE).durable(true).build();
    }

    // 声明队列
    @Bean
    public Queue handleMsgQueue() {
        return QueueBuilder.durable(VISIT_HANDLE_QUEUE).build();
    }

    // 绑定交换机和队列
    @Bean
    public Binding type1QueueBindingExchange(@Qualifier("handleMsgQueue") Queue handleMsgQueue,
                                             @Qualifier("visitMsgExchange") DirectExchange visitMsgExchange) {
        return BindingBuilder.bind(handleMsgQueue).to(visitMsgExchange).with(VISIT_HANDLE_RK);
    }
}

 redis incr自增方法

        incr命令如果指定的key中存储的值不是字符串类型或者存储的字符串类型不能表示为一个整数,那么执行这个命令时服务器会返回一个错误(ERR value is not an integer or out of range)。

        这里我使用的是StringRedisTemplate 而不是RedisTemplate,因为redisTemplate我已经用于存储对象类型,如果这里也用RedisTemplate来存则会报异常。

  • RedisTemplate使用的是 JdkSerializationRedisSerializer
  • StringRedisTemplate使用的是 StringRedisSerializer

        同时RedisTemplate和StringRedisTemplate两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。    

        或者是重写Redis序列化方式来解决

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    public Long incr(String key, long liveTime) {
        RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, stringRedisTemplate.getConnectionFactory());
        Long increment = entityIdCounter.getAndIncrement();
        if ((null == increment || increment.longValue() == 0) && liveTime > 0) {//初始设置过期时间
            entityIdCounter.expire(liveTime, TimeUnit.HOURS);
        }
        return increment;
    }

MQ发送消息

    @Autowired
    private RabbitTemplate rabbitTemplate;

    rabbitTemplate.convertAndSend(MQConfig.VISIT_COUNT_MSG_EXCHANGE,MQConfig.VISIT_HANDLE_RK,visitCountJson, msg -> {
                return msg;
        });

获得访问IP

    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    String ip = getRemortIP(request);

    public String getRemortIP(HttpServletRequest request) {
        if (request.getHeader("x-forwarded-for") == null) {
            return request.getRemoteAddr();
        }
        return request.getHeader("x-forwarded-for");
    }

 AOP

/**
 * @author 像向日葵一样~
 * @date 2022-08-03-14:39
 */
@Component
@Aspect
public class ApiVisitHistory {
    private Logger log = LoggerFactory.getLogger(ApiVisitHistory.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 定义切面
     */
    @Pointcut("execution(public * com.xxx.xxx.xxx.xxx(..)))")
    public void pointCut()    {
        ......

    }

    /**
     * 在接口原有的方法执行前,将会首先执行此处的代码
     */
    @Before("pointCut()")
    public void doBefore(JoinPoint joinPoint) {
        ......
    }

    /**
     * 只有正常返回才会执行此方法
     * 如果程序执行失败,则不执行此方法
     */
    @AfterReturning(returning = "returnVal", pointcut = "pointCut()")
    public void doAfterReturning(JoinPoint joinPoint, Object returnVal) {

       //封装好消息发送给MQ
       ......
    }

    /**
     * 当接口报错时执行此方法
     */
    @AfterThrowing(pointcut = "pointCut()")
    public void doAfterThrowing(JoinPoint joinPoint) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        log.info("接口访问失败,URI:[{}]", request.getRequestURI());
    }

}

根据IP查询地址

import org.lionsoul.ip2region.xdb.Searcher;
import java.util.concurrent.TimeUnit;

/**
 * ip 转 ip归属地工具类
 * @author 像向日葵一样~
 * @date 2022-08-03-15:21
 */
public class Ip2regionUtils {

    private Ip2regionUtils() {
    }

    /**
     * 系统默认的ip库文件名
     */
    private static final String DB_NAME = "ip2region.xdb";


    public static String searchAddrByIp(String ip,String xdbRealPath){
        // 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
        byte[] vIndex = null;
            String dbPath = xdbRealPath+DB_NAME;

        try {
            vIndex = Searcher.loadVectorIndexFromFile(dbPath);
        } catch (Exception e) {
            System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
        }

        // 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
        Searcher searcher = null;
        String region;
        try {
            searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
        } catch (Exception e) {
            System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
        }

        // 3、查询
        try {
            long sTime = System.nanoTime();
            region = searcher.search(ip);
            long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
            System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
            return region;
        } catch (Exception e) {
            System.out.printf("failed to search(%s): %s\n", ip, e);
        }
        return null;
    }

}

         我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询

import org.lionsoul.ip2region.xdb.Searcher;
import java.util.concurrent.TimeUnit;

/**
 * ip 转 ip归属地工具类
 * @author 像向日葵一样~
 * @date 2022-08-03-15:21
 */
public class Ip2regionUtils {

    private Ip2regionUtils() {
    }

    /**
     * 系统默认的ip库文件名
     */
    private static final String DB_NAME = "ip2region.xdb";


   public static String searchAddrByIp(String ip,String xdbRealPath) {
        // 1、从 dbPath 加载整个 xdb 到内存。
        byte[] cBuff;
        String dbPath = xdbRealPath+DB_NAME;
        try {
            cBuff = Searcher.loadContentFromFile(dbPath);
            // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
            Searcher searcher;
            // 3、查询
            searcher = Searcher.newWithBuffer(cBuff);
            long sTime = System.nanoTime();
            String region = searcher.search(ip);
            long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
            System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
            return region;
        } catch (Exception e) {
            System.out.printf("failed to search(%s): %s\n", ip, e);
        }
        // 4、关闭资源 - 该 searcher 对象可以安全用于并发,等整个服务关闭的时候再关闭 searcher
        // searcher.close();
        // 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
        return null;
    }

}

MQ监听消息

/**
 * @author 像向日葵一样~
 * @date 2022-08-03-16:10
 */
@Component
public class VisitMsgConsumer {

    @Value("${ipxdb.profile}")
    private String xdbPath;


    @RabbitListener(queues = MQConfig.VISIT_HANDLE_QUEUE) // 访问IP处理消息队列
    public void receiveMsgFromBuyerQueue(Message message) {
        VisitCountDTO visitCountDTO = (VisitCountDTO)JsonUtil.fromJson(new String(message.getBody()), VisitCountDTO.class);
        String ipAddress = Ip2regionUtils.searchAddrByIp(visitCountDTO.getIp(),xdbPath);
        //接收到消息及获得IP、IP地址后,接着跟数据库进行交互
        ......
    }
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐