跳过正文
  1. 文章/

使用 Spring Boot 实现博客统计服务

haifeiWu
作者
haifeiWu
北京后端开发者,热爱分布式系统、中间件与 AI 工程,正在深入学习 Go 与 Rust。
目录

📌 本文原发布于掘金社区:使用 Spring Boot 实现博客统计服务

作为一个后端开发,在微服务,server mesh 等概念满天飞的时代,持续学习能力是不能丢的,因此楼主最近也研究好多 RPC,Netty,Spring Boot 等技术。此外,楼主博客的阅读统计功能用的是与 HEXO 相匹配的第三方的数量统计功能,也就诞生了楼主这次更换成自己开发的基础功能的装逼之旅。

通过 SPRING INITIALIZR 生成工程
#

spring initSpring init
如上图,通过 Spring 官方的 Spring Initial 网站生成项目,项目的目录结构如下:

  • 目录结构
- src
    -main
        -java
            -package
                #主函数,启动类,运行它如果运行了 Tomcat、Jetty、Undertow 等容器
                -SpringbootApplication    
        -resouces
            #存放静态资源 js/css/images 等
            - statics
            #存放 html 模板文件
            - templates
            #主要的配置文件,SpringBoot启动时候会自动加载application.yml/application.properties        
            - application.yml
    #测试文件存放目录        
    -test
 # pom.xml 文件是Maven构建的基础,里面包含了我们所依赖JAR和Plugin的信息
- pom
  • pom 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.*</groupId>
    <artifactId>*-*</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>base-service</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.6</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.almende.eve</groupId>
            <artifactId>eve-bundle-full</artifactId>
            <version>3.1.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

实现 Redis 存储逻辑
#

选择 Redis 而没选择数据库的原因是 Redis 提供了丰富的数据结构与数据持久化策略,另外 Redis 是基于内存的,相对于数据库来说,快了不止一个数量级。而统计阅读次数的场景对接口处理的速度还是有一定的要求的,因此楼主选择了 Redis 作为阅读次数统计的 db。
下面就是 Redis 操作的基础代码,比较简单,楼主贴一下代码,不做进一步的阐述

  • Redis 的接口类

    public interface RedisService {
        public boolean set(final String key, final String value);
        public String get(final String key);
        public String incr(final String key);
    }
    
  • Redis 的实现类

@Service
public class RedisServiceImpl implements RedisService {

    @Resource
    private RedisTemplate<String, ?> redisTemplate;

    @Override
    public boolean set(final String key, final String value) {
        boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
                connection.set(serializer.serialize(key), serializer.serialize(value));
                return true;
            }
        });
        return result;
    }

    @Override
    public String incr(final String key) {
        String incr = redisTemplate.execute(new RedisCallback<String>() {
            @Override
            public String doInRedis(RedisConnection redisConnection) throws DataAccessException {
                RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
                Long value = redisConnection.incr(serializer.serialize(key));
                return String.valueOf(value);
            }
        });
        return incr;
    }

    @Override
    public String get(final String key){
        String result = redisTemplate.execute(new RedisCallback<String>() {
            @Override
            public String doInRedis(RedisConnection connection) throws DataAccessException {
                RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
                byte[] value =  connection.get(serializer.serialize(key));
                return serializer.deserialize(value);
            }
        });
        return result;
    }

}

博客阅读次数统计接口实现
#

博客阅读次数统计的基本业务逻辑就是,每篇博客对应的 blogId 作为 Redis 的 key,而访问次数就是这个 key 所对应的 value,每访问一次该接口就要将对应的 blogId 自增一次,并返回对应的 value。这里楼主选择的 Redis 的数据结构是 Redis 的 String,下面是楼主实现该逻辑的主要代码:

/**
 * 统计博客阅读次数.
 *
 * @author wuhf
 * @Date 2018/6/15 15:59
 **/
@RestController
@RequestMapping("/")
public class BlogReadCountController {

    private static String ALLOW_REQUEST_URL = "******";
    private static String ILLEGAL_CHARACTERS = "*";
    private static String DEFAULT_READ_COUNT = "1";
    private static Logger logger = LoggerFactory.getLogger(BlogReadCountController.class);

    @Autowired
    private RedisService redisService;

    @ResponseBody
    @RequestMapping("/*_*")
    public ResultCode blogReadCountIncr(HttpServletRequest request,String blogId) {
        ResultCode resultCode = new ResultCode();
        try {
            logger.info(">>>>>> method blogReadCountIncr exec , request params is : {}",blogId);
            String readCount = redisService.get(blogId);
            if (StringUtils.isBlank(readCount)) {
                
                if (!blogId.startsWith(ALLOW_REQUEST_URL)||blogId.contains(ILLEGAL_CHARACTERS)) {
                    resultCode.setCode(Messages.API_ERROR_CODE);
                    resultCode.setMsg(Messages.API_ERROR_MSG);
                    return resultCode;
                }

                redisService.set(blogId,DEFAULT_READ_COUNT);
                readCount = DEFAULT_READ_COUNT;
            } else {
                readCount = redisService.incr(blogId);
            }
            logger.info(">>>>>> readCount is : {}",readCount);
            resultCode.setCode(Messages.SUCCESS_CODE);
            resultCode.setMsg(Messages.SUCCESS_MSG);
            resultCode.setData(readCount);
            return resultCode;
        } catch (Exception e) {
            e.printStackTrace();
            resultCode.setCode(Messages.API_ERROR_CODE);
            resultCode.setMsg(Messages.API_ERROR_MSG);
            return resultCode;
        }
    }
}

实现过程中遇到的坑
#

  • 设置应用启动的端口号
server.port=9401
  • 设置应用访问的 path

Spring Boot 应用默认的应用访问的 path 还是 “/“,楼主在这里吃了点苦头,使用http://项目名:port访问服务,愣是访问不通,在配置文件中设置如下所示

server.servlet.context-path=/项目名

小结
#

目前很多大佬都写过关于 SpringBoot 的教程了,如有雷同,请略过不看,本文通过自己的亲身实战以及楼主自己踩到的坑完成的,另外本文是基于最新的 spring-boot-starter-parent:2.0.3.RELEASE 编写。

号外
#

楼主造了一个轮子,LIGHTCONF 是一个基于 Netty 实现的配置管理平台,其核心设计目标是“为业务提供统一的配置管理服务”,可以做到开箱即用。感兴趣的给个 star 支持一下。

相关文章