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

2,488 阅读4分钟
原文链接: www.hchstudio.cn

作为一个后端开发,在微服务,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的Stirng,下面是楼主实现该逻辑的主要代码:



/**
 * 统计博客阅读次数.
 *
 * @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支持一下。