2024年8月

一、前言

大家好呀,我是summo,前面已经教会大家怎么去阿里云买服务器(
链接在这,需要自取
),以及怎么搭建JDK、Redis、MySQL这些环境或者数据库。从这篇文章开始就进入正式的编码阶段了,我们从后端开始,先把热搜数据获取到,然后再开始前端部分。

本来我想把后端应用搭建和完成第一个爬虫分为两篇文章写的,但是想到墨迹三周还没看到效果,估计有些同学已经等不及了,所以我把这两篇文章合成一篇了,篇幅比较长,感兴趣的同学可以认真看下。因为后端应用初始化的时候涉及很多配置,有很多命名,建议大家写别急着个性化用自己的名字命名应用和配置,可以先用我的命名方式搭建一个出来,先成功再研究个性化,这样成就感很足,学习动力也强。

二、后端应用搭建

这个
摸鱼小网站
主要使用的SpringBoot框架配合一些中间件实现,开发工具用的是idea社区版,建议不要下载人家的正式版然后破解,一来麻烦的很,二来社区版足够开发使用了,
点击这个可以下载idea社区版

1. maven项目搭建

打开idea后,点击New Project,我们从一个空的maven项目开始,一步步把项目搭建出来。

我们输入name、GroupId、ArtifactId点击确定,如果你是老手可以自己搭建,如果你是新手
建议跟我输的一样,因为后面还有很多地方要用的这些名字

把src目录删掉,我们不需要,只留下pom.xml文件就行了。

接下来我们开始创建子module,创建方式如下图:

以summo-sbmy-start子module为例,输入如下的信息就可以创建一个module出来啦。

按照这个方式创建出如下的module出来。

2. pom.xml配置

这部分我现在不详细说,后面我会单独写一篇文章讲我的依赖和配置。如果刚才你的名字取得跟我一样,直接复制粘贴就完事了,如果名字不一样记得把名字给替换为你的。

(1)summo-sbmy

pom.xml内容如下:

<?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>

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

    <groupId>com.summo</groupId>
    <artifactId>summo-sbmy</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>summo-sbmy-dao</module>
        <module>summo-sbmy-service</module>
        <module>summo-sbmy-web</module>
        <module>summo-sbmy-start</module>
        <module>summo-sbmy-job</module>
        <module>summo-sbmy-common</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>

            <!-- 自依赖 -->
            <dependency>
                <groupId>com.summo</groupId>
                <artifactId>summo-sbmy-common</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.summo</groupId>
                <artifactId>summo-sbmy-service</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.summo</groupId>
                <artifactId>summo-sbmy-common</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.summo</groupId>
                <artifactId>summo-sbmy-dao</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.summo</groupId>
                <artifactId>summo-sbmy-job</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.summo</groupId>
                <artifactId>summo-sbmy-start</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.summo</groupId>
                <artifactId>summo-sbmy-web</artifactId>
                <version>${project.version}</version>
            </dependency>

            <!-- xxl-job -->
            <dependency>
                <groupId>com.xuxueli</groupId>
                <artifactId>xxl-job-core</artifactId>
                <version>2.2.0</version>
            </dependency>

            <!-- MySQL驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.20</version>
            </dependency>

            <!-- mybatis-plus驱动 -->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.3.2</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-extension</artifactId>
                <version>3.3.2</version>
            </dependency>

            <!-- 分页插件 -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>1.4.1</version>
            </dependency>

            <!-- lombok -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.16.22</version>
            </dependency>

            <!-- druid链接池 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.1.22</version>
            </dependency>

            <!-- aspectj -->
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>1.9.5</version>
            </dependency>

            <!-- fastjson -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>2.0.21</version>
            </dependency>

            <!-- 线程上下文 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>transmittable-thread-local</artifactId>
                <version>2.11.1</version>
            </dependency>

            <!-- 接口参数校验 -->
            <dependency>
                <groupId>javax.validation</groupId>
                <artifactId>validation-api</artifactId>
                <version>2.0.1.Final</version>
            </dependency>

            <!-- Redisson分布式锁 -->
            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson-spring-boot-starter</artifactId>
                <version>3.24.0</version>
            </dependency>

            <!-- 通用工具类 -->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.5</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-collections4</artifactId>
                <version>4.1</version>
            </dependency>

            <!-- VM 模板 -->
            <dependency>
                <groupId>com.alibaba.boot</groupId>
                <artifactId>velocity-spring-boot-starter</artifactId>
                <version>1.0.4.RELEASE</version>
            </dependency>

            <!-- guava -->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>32.1.1-jre</version>
            </dependency>

            <!-- httpclient -->
            <dependency>
                <groupId>org.apache.httpcomponents</groupId>
                <artifactId>httpcore</artifactId>
                <version>4.4.16</version>
            </dependency>
            <dependency>
                <groupId>org.apache.httpcomponents</groupId>
                <artifactId>httpclient</artifactId>
                <version>4.5.14</version>
            </dependency>

            <!-- jsoup -->
            <dependency>
                <groupId>org.jsoup</groupId>
                <artifactId>jsoup</artifactId>
                <version>1.12.1</version>
            </dependency>

            <!-- ip2region  -->
            <dependency>
                <groupId>org.lionsoul</groupId>
                <artifactId>ip2region</artifactId>
                <version>2.6.3</version>
            </dependency>

            <!-- 用于读取ip2region.xdb文件使用 -->
            <dependency>
                <groupId>commons-io</groupId>
                <artifactId>commons-io</artifactId>
                <version>2.6</version>
            </dependency>

            <!-- 加解密代码-->
            <dependency>
                <groupId>org.bouncycastle</groupId>
                <artifactId>bcprov-jdk15on</artifactId>
                <version>1.68</version>
            </dependency>
            <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
                <version>1.15</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>utf-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>2.2</version>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <finalName>${project.build.finalName}</finalName>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

(2)summo-sbmy-start

pom.xml内容如下:

<?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">
    <parent>
        <artifactId>summo-sbmy</artifactId>
        <groupId>com.summo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>summo-sbmy-start</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.summo</groupId>
            <artifactId>summo-sbmy-service</artifactId>
        </dependency>
        <dependency>
            <groupId>com.summo</groupId>
            <artifactId>summo-sbmy-job</artifactId>
        </dependency>
        <dependency>
            <groupId>com.summo</groupId>
            <artifactId>summo-sbmy-dao</artifactId>
        </dependency>
        <dependency>
            <groupId>com.summo</groupId>
            <artifactId>summo-sbmy-common</artifactId>
        </dependency>
        <dependency>
            <groupId>com.summo</groupId>
            <artifactId>summo-sbmy-web</artifactId>
        </dependency>

        <!-- 测试组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!-- 添加四个环境的变量,变量名为environment -->
    <profiles>
        <profile>
            <id>test</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <environment>test</environment>
            </properties>
        </profile>
        <profile>
            <id>consumer</id>
            <properties>
                <environment>consumer</environment>
            </properties>
        </profile>
        <profile>
            <id>producer</id>
            <properties>
                <environment>producer</environment>
            </properties>
        </profile>
    </profiles>

    <build>
        <finalName>summo-sbmy</finalName>
        <resources>
            <resource>
                <!-- 指定配置文件所在的resource目录 -->
                <directory>src/main/resources</directory>
                <includes>
                    <include>application.properties</include>
                    <include>logback-spring.xml</include>
                    <include>**/*.html</include>
                    <include>**/*.js</include>
                    <include>**/*.css</include>
                </includes>
                <filtering>true</filtering>
            </resource>
            <resource>
                <!-- 指定配置文件所在的resource目录 -->
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.woff</include>
                    <include>**/*.ttf</include>
                    <include>**/*.xdb</include>
                    <include>**/*.jks</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.1.13.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>com.summo.sbmy.Application</mainClass>
                </configuration>
            </plugin>
            <!-- 解压fat jar到target/${project-name}目录 -->
            <plugin>
                <artifactId>maven-antrun-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <configuration>
                            <target>
                                <unzip
                                        src="${project.build.directory}/${project.build.finalName}.${project.packaging}"
                                        dest="${project.build.directory}/summo-sbmy"/>
                            </target>
                        </configuration>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

(3)summo-sbmy-common

pom.xml内容如下:

<?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">
    <parent>
        <artifactId>summo-sbmy</artifactId>
        <groupId>com.summo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>summo-sbmy-common</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- SpringBoot框架 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- aspectj -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>

        <!-- logback核心组件 -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
        </dependency>

        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

        <!-- 线程上下文 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
        </dependency>
        <!-- 分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.4.1</version>
        </dependency>
        <!-- 接口参数校验 -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>

        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
        </dependency>
        <!-- Redis框架 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- Redisson分布式锁 -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
        </dependency>

        <!-- jedisÏ分布式锁 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <!-- 通用工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
        </dependency>

        <!-- guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>

        <!-- httpclient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
        </dependency>

        <!-- ip2region  -->
        <dependency>
            <groupId>org.lionsoul</groupId>
            <artifactId>ip2region</artifactId>
        </dependency>

        <!-- 用于读取ip2region.xdb文件使用 -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>

        <!-- 加解密代码 -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.6</version>
        </dependency>

    </dependencies>
</project>

(4)summo-sbmy-dao

pom.xml内容如下:

<?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">
    <parent>
        <artifactId>summo-sbmy</artifactId>
        <groupId>com.summo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>summo-sbmy-dao</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.summo</groupId>
            <artifactId>summo-sbmy-common</artifactId>
        </dependency>
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- mybatis-plus驱动 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-extension</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>javax.persistence-api</artifactId>
        </dependency>

        <!-- druid链接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.6</version>
                <configuration>
                    <configurationFile>${basedir}/src/main/resources/generator/generatorConfiguration.xml
                    </configurationFile>
                    <overwrite>true</overwrite>
                    <verbose>true</verbose>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>tk.mybatis</groupId>
                        <artifactId>mapper</artifactId>
                        <version>4.1.2</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>

(5)summo-sbmy-service

pom.xml内容如下:

<?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">
    <parent>
        <artifactId>summo-sbmy</artifactId>
        <groupId>com.summo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>summo-sbmy-service</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.summo</groupId>
            <artifactId>summo-sbmy-dao</artifactId>
        </dependency>
    </dependencies>
</project>

(7)summo-sbmy-web

pom.xml内容如下:

<?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">
    <parent>
        <artifactId>summo-sbmy</artifactId>
        <groupId>com.summo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>summo-sbmy-web</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.summo</groupId>
            <artifactId>summo-sbmy-service</artifactId>
        </dependency>
        <!-- thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
</project>

pom.xml配置贴好后,更新一下,把依赖包都下载下来。如果你发现下载的非常慢,把maven仓库的镜像换一下,换成阿里云的。镜像配置如下:

<mirrors>
  <mirror>
    <id>alimaven</id>
    <mirrorOf>central</mirrorOf>
    <name>aliyun maven</name>
    <url>https://maven.aliyun.com/repository/public</url>
  </mirror>
</mirrors>

3. application.properties配置

## 应用名
spring.application.name=summo-sbmy
## 端口号
server.port=8080

# 配置Druid数据源类型
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# 数据库连接URL,包括数据库名、允许的公开密钥检索、字符编码、禁用SSL、时区设置等
spring.datasource.url=jdbc:mysql://xxx:3306/summo-sbmy?allowPublicKeyRetrieval=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&zeroDateTimeBehavior=convertToNull
# 数据库用户名
spring.datasource.username=xxx
# 数据库密码
spring.datasource.password=xxx
# 数据库驱动类名
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 初始化连接池时创建的连接数量
spring.datasource.druid.initial-size=5
# 连接池最大活跃连接数
spring.datasource.druid.max-active=30
# 连接池最小空闲连接数
spring.datasource.druid.min-idle=5
# 等待连接获取的最大等待时间
spring.datasource.druid.max-wait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一个连接在池中最小生存的时间
spring.datasource.druid.min-evictable-idle-time-millis=300000
# 验证数据库连接有效的SQL语句
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
# 是否在从连接池获取连接前进行检验(建议关闭,影响性能)
spring.datasource.druid.test-while-idle=true
# 获取连接时执行validationQuery检测连接是否有效(建议关闭,影响性能)
spring.datasource.druid.test-on-borrow=false
# 归还连接时执行validationQuery检测连接是否有效(建议关闭,影响性能)
spring.datasource.druid.test-on-return=false
# 是否开启PSCache(PreparedStatement缓存),默认false
spring.datasource.druid.pool-prepared-statements=false
# 指定每个连接上PSCache的大小,默认-1表示不限制
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=0
# 启用监控统计和日志过滤器
spring.datasource.druid.filters=stat,wall
# 配置StatFilter的参数,合并SQL记录
spring.datasource.druid.connection-properties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# 是否合并Druid数据源的监控信息
spring.datasource.druid.use-global-data-source-stat=true
# 开启Wall过滤器,并指定数据库类型为MySQL
spring.datasource.druid.filter.wall.enabled=true
spring.datasource.druid.filter.wall.db-type=mysql
# 配置StatFilter的数据库类型为MySQL
spring.datasource.druid.filter.stat.db-type=mysql
# 开启StatFilter
spring.datasource.druid.filter.stat.enabled=true

# MyBatis配置:自动映射行为设置为全字段
mybatis.configuration.auto-mapping-behavior=full
# MyBatis配置:下划线转驼峰命名规则
mybatis.configuration.map-underscore-to-camel-case=true
# MyBatis-Plus的Mapper文件位置
mybatis-plus.mapper-locations=classpath*:/mybatis/mapper/*.xml

# Redis数据库索引
spring.redis.database=0
# Redis连接超时时间(毫秒)
spring.redis.timeout=1800000
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器端口
spring.redis.port=6379
# Redis服务器连接密码
spring.redis.password=xxx
# 使用Lettuce连接池时的最大等待时间(-1表示无限制)
spring.redis.lettuce.pool.max-wait=-1
# Lettuce连接池的最大空闲连接数
spring.redis.lettuce.pool.max-idle=5
# Lettuce连接池的最小空闲连接数
spring.redis.lettuce.pool.min-idle=0
# Lettuce连接池的最大活跃连接数
spring.redis.lettuce.pool.max-active=20
# 使用Jedis连接池时的最小空闲连接数
spring.redis.jedis.pool.min-idle=8
# Jedis连接池的最大空闲连接数
spring.redis.jedis.pool.max-idle=500
# Jedis连接池的最大活跃连接数
spring.redis.jedis.pool.max-active=2000
# Jedis连接池的最大等待时间(毫秒)
spring.redis.jedis.pool.max-wait=10000

4. logback-spring.xml配置

<configuration>
    <!-- 默认的一些配置 -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <!-- 定义应用名称,区分应用 -->
    <property name="APP_NAME" value="summo-sbmy"/>
    <!-- 定义日志文件的输出路径 -->
    <property name="LOG_PATH" value="${user.home}/logs/${APP_NAME}"/>
    <!-- 定义日志文件名称和路径 -->
    <property name="LOG_FILE" value="${LOG_PATH}/application.log"/>
    <!-- 定义警告级别日志文件名称和路径 -->
    <property name="WARN_LOG_FILE" value="${LOG_PATH}/warn.log"/>
    <!-- 定义错误级别日志文件名称和路径 -->
    <property name="ERROR_LOG_FILE" value="${LOG_PATH}/error.log"/>

    <!-- 自定义控制台打印格式 -->
    <property name="FILE_LOG_PATTERN" value="%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%blue(requestId: %X{requestId})] [%highlight(%thread)] ${PID:- } %logger{36} %-5level - %msg%n"/>

    <!-- 将日志滚动输出到application.log文件中 -->
    <appender name="APPLICATION"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 输出文件目的地 -->
        <file>${LOG_FILE}</file>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
        <!-- 设置 RollingPolicy 属性,用于配置文件大小限制,保留天数、文件名格式 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 文件命名格式 -->
            <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 文件保留最大天数 -->
            <maxHistory>7</maxHistory>
            <!-- 文件大小限制 -->
            <maxFileSize>50MB</maxFileSize>
            <!-- 文件总大小 -->
            <totalSizeCap>500MB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <!-- 摘取出WARN级别日志输出到warn.log中 -->
    <appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${WARN_LOG_FILE}</file>
        <encoder>
            <!-- 使用默认的输出格式打印 -->
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
        <!-- 设置 RollingPolicy 属性,用于配置文件大小限制,保留天数、文件名格式 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 文件命名格式 -->
            <fileNamePattern>${LOG_PATH}/warn.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 文件保留最大天数 -->
            <maxHistory>7</maxHistory>
            <!-- 文件大小限制 -->
            <maxFileSize>50MB</maxFileSize>
            <!-- 文件总大小 -->
            <totalSizeCap>500MB</totalSizeCap>
        </rollingPolicy>
        <!-- 日志过滤器,将WARN相关日志过滤出来 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>WARN</level>
        </filter>
    </appender>

    <!-- 摘取出ERROR级别日志输出到error.log中 -->
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${ERROR_LOG_FILE}</file>
        <encoder>
            <!-- 使用默认的输出格式打印 -->
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
        <!-- 设置 RollingPolicy 属性,用于配置文件大小限制,保留天数、文件名格式 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 文件命名格式 -->
            <fileNamePattern>${LOG_PATH}/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 文件保留最大天数 -->
            <maxHistory>7</maxHistory>
            <!-- 文件大小限制 -->
            <maxFileSize>50MB</maxFileSize>
            <!-- 文件总大小 -->
            <totalSizeCap>500MB</totalSizeCap>
        </rollingPolicy>
        <!-- 日志过滤器,将ERROR相关日志过滤出来 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
    </appender>

    <!-- 配置控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>


    <!-- 配置输出级别 -->
    <root level="INFO">
        <!-- 加入控制台输出 -->
        <appender-ref ref="CONSOLE"/>
        <!-- 加入APPLICATION输出 -->
        <appender-ref ref="APPLICATION"/>
        <!-- 加入WARN日志输出 -->
        <appender-ref ref="WARN"/>
        <!-- 加入ERROR日志输出 -->
        <appender-ref ref="ERROR"/>
    </root>
</configuration>

5. 创建启动类

package com.summo.sbmy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * @author summo
 * @version Application.java, 1.0.0
 * @description 启动核心类
 * @date 2024年08月09
 */
@SpringBootApplication(scanBasePackages = {"com.summo.sbmy"})
@EnableScheduling
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

点击启动,打印如下就算成功

三、实现抖音热搜爬虫

第一次写爬虫代码,我们找一个最简单,也是最安全的例子,
抖音热搜

1. 爬虫方案评估

为什么简单?
它就一个接口:
https://www.iesdouyin.com/web/api/v2/hotsearch/billboard/word/
,免登录也不需要参数,你们直接在浏览器上调用就可以把抖音的热搜数据获取到了;

为什么安全?
调用
https://www.iesdouyin.com/robots.txt
接口,返回如下:

User-agent: *
Allow: /

Sitemap: http://www.iesdouyin.com/sitemap/index.xml

这个代表什么呢?

看到了吗,人家不仅允许你去爬,还提供了网站地图的链接,这也是他们为了提高谷歌、必应、百度等浏览器的收录的一种办法。所以,不用担心爬这些数据会怎么样了。

2. 获取链接的cURL代码

先上一张图,如下:

大概的步骤如下:

  1. 输入
    https://www.iesdouyin.com/web/api/v2/hotsearch/billboard/word/
    链接,按下enter;
  2. 打开控制台,选择【全部】找到刚才调用的接口;
  3. 选中接口,右键打开菜单,选择复制里面的“以cURL格式复制”这一项。

复制出来是这样的东西

curl 'https://www.iesdouyin.com/web/api/v2/hotsearch/billboard/word/' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \
  -H 'accept-language: zh-CN,zh;q=0.9' \
  -H 'cache-control: no-cache' \
  -H 'cookie: ttwid=1%7CJ6ehEognyMAob_gD6oZwA40monN8E_sENr3IUZmuk7o%7C1712472728%7C44b0cd0003fb75861789d62e56f014eaea3d198898a0ae9a947bf61d95d8ac1a; __ac_signature=_02B4Z6wo00f01fFoqvgAAIDBFmj97SX8qiXxSK5AABr708; __ac_referer=https://pre-dc-console.alibaba-inc.com/' \
  -H 'pragma: no-cache' \
  -H 'priority: u=0, i' \
  -H 'sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "macOS"' \
  -H 'sec-fetch-dest: document' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-site: none' \
  -H 'sec-fetch-user: ?1' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'

能看懂吗?能看懂最好,看不懂也没有关系,不是给我们看的,给Postman看的。Postman是一个http接口调用工具非常好用,电脑上没有Postman的同学就去下载一个。

3. 使用Postman生成调用代码

打开你的Postman软件,按照我下面这张图操作:

按照我上面的步骤,将cURL命令导入Postman,可以快速生成一个调用请求,如下图:

这个方式也可以用在我们平时调试接口,比如说有一个接口报错了,需要不断地叫前端重试一下,再重试一下,非常麻烦。这个时候你就可以叫前端把这个cURL复制给你,自己动手重试,就不用麻烦别人啦。

在Postman的右上角有一个
Code snippet
,可以直接生成你想要的调用代码,啥Java、Python、NodeJs都有,不用自己写,复制就可以运行。咋样,Postman没有白骗你下载吧!

代码如下

OkHttpClient client = new OkHttpClient().newBuilder()
  .build();
Request request = new Request.Builder()
  .url("https://www.iesdouyin.com/web/api/v2/hotsearch/billboard/word/")
  .method("GET", null)
  .addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
  .addHeader("accept-language", "zh-CN,zh;q=0.9")
  .addHeader("cache-control", "no-cache")
  .addHeader("cookie", "ttwid=1%7CJ6ehEognyMAob_gD6oZwA40monN8E_sENr3IUZmuk7o%7C1712472728%7C44b0cd0003fb75861789d62e56f014eaea3d198898a0ae9a947bf61d95d8ac1a; __ac_signature=_02B4Z6wo00f01fFoqvgAAIDBFmj97SX8qiXxSK5AABr708; __ac_referer=https://pre-dc-console.alibaba-inc.com/; ttwid=1%7CX9ppA_NoTHJI9DG3JN7wNnZ662r-aJbZwCFPLLGK-og%7C1713836331%7Cdbc79a439d0ecc994f60043d66b4ad3ff81c3820f3ab83ef85d30875cc59a18b")
  .addHeader("pragma", "no-cache")
  .addHeader("priority", "u=0, i")
  .addHeader("sec-ch-ua", "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\"")
  .addHeader("sec-ch-ua-mobile", "?0")
  .addHeader("sec-ch-ua-platform", "\"macOS\"")
  .addHeader("sec-fetch-dest", "document")
  .addHeader("sec-fetch-mode", "navigate")
  .addHeader("sec-fetch-site", "none")
  .addHeader("sec-fetch-user", "?1")
  .addHeader("upgrade-insecure-requests", "1")
  .addHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
  .build();
Response response = client.newCall(request).execute();

4. 将代码迁移到应用中


summo-sbmy-job
这个module下,创建一个文件夹
com.summo.sbmy.job.douyin
,创建DouyinHotSearchJob.java,代码如下

package com.summo.sbmy.job.douyin;

import java.io.IOException;

import com.alibaba.fastjson.JSONObject;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author summo
 * @version DouyinHotSearchJob.java, 1.0.0
 * @description 抖音热搜Java爬虫代码
 * @date 2024年08月09
 */
@Component
public class DouyinHotSearchJob {

    /**
     * 定时触发爬虫方法,1个小时执行一次
     */
    @Scheduled(fixedRate = 1000 * 60 * 60)
    public void hotSearch() throws IOException {
        OkHttpClient client = new OkHttpClient().newBuilder()
            .build();
        Request request = new Request.Builder()
            .url("https://www.iesdouyin.com/web/api/v2/hotsearch/billboard/word/")
            .method("GET", null)
            .addHeader("accept",
                       "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;"
                           + "q=0.8,application/signed-exchange;v=b3;q=0.7")
            .addHeader("accept-language", "zh-CN,zh;q=0.9")
            .addHeader("cache-control", "no-cache")
            .addHeader("cookie",
                       "ttwid=1%7CJ6ehEognyMAob_gD6oZwA40monN8E_sENr3IUZmuk7o%7C1712472728"
                           + "%7C44b0cd0003fb75861789d62e56f014eaea3d198898a0ae9a947bf61d95d8ac1a; "
                           + "__ac_signature=_02B4Z6wo00f01fFoqvgAAIDBFmj97SX8qiXxSK5AABr708; "
                           + "__ac_referer=https://pre-dc-console.alibaba-inc.com/; "
                           + "ttwid=1%7CX9ppA_NoTHJI9DG3JN7wNnZ662r-aJbZwCFPLLGK-og%7C1713836331"
                           + "%7Cdbc79a439d0ecc994f60043d66b4ad3ff81c3820f3ab83ef85d30875cc59a18b")
            .addHeader("pragma", "no-cache")
            .addHeader("priority", "u=0, i")
            .addHeader("sec-ch-ua", "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\"")
            .addHeader("sec-ch-ua-mobile", "?0")
            .addHeader("sec-ch-ua-platform", "\"macOS\"")
            .addHeader("sec-fetch-dest", "document")
            .addHeader("sec-fetch-mode", "navigate")
            .addHeader("sec-fetch-site", "none")
            .addHeader("sec-fetch-user", "?1")
            .addHeader("upgrade-insecure-requests", "1")
            .addHeader("user-agent",
                       "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) "
                           + "Chrome/126.0.0.0 Safari/537.36")
            .build();
        Response response = client.newCall(request).execute();
        System.out.println(JSONObject.toJSONString(response.body().string()));
    }

}

这个爬虫代码会在启动的时候执行一次,然后每一小时执行一次。这样,我们的第一个定时爬虫就做好了。如果以上的内容你都看不懂,那就直接复制我这代码吧,鱼和渔都给你了。

四、小结一下

这一篇配置文件很多,篇幅很长,大家要耐心和细心一些,不然很容易出错。这里给出的配置是按照最终版给的,到时开发的时候不用担心少依赖或者少包,至于原理和选型后面再单独说吧。我觉得看一个Java程序员经验丰不丰富从他搭建的脚手架就可以看出来,因为脚手架不像代码一样有标准,它是由框架和插件构成的,适合你就用,没有什么是必须的。

还有就是爬虫,可能有些同学会失望,这玩意咋这么简单,一点技术含量都没有。简单的原因一是热搜接口都是免登无校验的,二是因为我已经给你们趟了一条路出来,饭喂到嘴里了。后面还有很多热搜爬虫,有些也挺麻烦的,不用急,我会慢慢公布。
大多数同学大多数时间都只是在干CRUD的活,也没有独立建站的经验,虽然没有但是可以学!现在不会练练就会!100块钱的实操经验绝对比100块钱买的专栏更有意义更有用!

最后,
自建摸鱼网站
,各大网站热搜一览,上班和摸鱼很配哦!

开心一刻

前两天有个女生加我,我同意了

第一天,她和我聊文学,聊理想,聊篮球,聊小猫小狗

第二天,她和我说要看我腹肌

吓我一跳,我反手就删除拉黑,我特喵一肚子的肥肉,哪来的腹肌!

就离谱

循环依赖

关于 Spring 的循环依赖,我已经写了 4 篇

Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗

再探循环依赖 → Spring 是如何判定原型循环依赖和构造方法循环依赖的

三探循环依赖 → 记一次线上偶现的循环依赖问题

四探循环依赖 → 当循环依赖遇上 BeanPostProcessor,爱情可能就产生了!

此时你们是不是有点慌,莫非要来五探了,还有完没完了?我先给你们打一针强心剂,今天我们不聊循环依赖,而是来看看在调试循环依赖过程中遇到的小插曲

首先声明下,这是来自园友(@飞的很慢的牛蛙 )的素材,已经过他同意

循环依赖案例很简单

pom.xml

<?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>
    <parent>
        <groupId>com.qsl</groupId>
        <artifactId>spring-circle</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>spring-circle-async</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
    </dependencies>
</project>

Spring 的版本用的是:
5.2.12.RELEASE

Circle.java

/**
 * @author: 青石路
 */
@Component
public class Circle {

    @Autowired
    private Loop loop;

    public Loop getLoop() {
        return loop;
    }

    public void sayHello(String name) {
        System.out.println("circle sayHello, " + name);
    }
}

Loop.java

/**
 * @author: 青石路
 */
@Component
public class Loop {

    @Autowired
    @Lazy
    private Circle circle;

    public Circle getCircle() {
        return circle;
    }

    public void sayHello(String name) {
        System.out.println("loop sayHello, " + name);
    }
}

为了兼容 Spring 的各种版本,加了
@Lazy

CircleTest.java

/**
 * @author: 青石路
 */
@ComponentScan(basePackages = "com.qsl")
public class CircleTest {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(CircleTest.class);
        Circle circle = ctx.getBean(Circle.class);
        Loop loop = ctx.getBean(Loop.class);
        System.out.println(circle.getLoop());
        System.out.println(loop);
    }
}

main
跑起来是没问题滴

完整代码:
spring-circle-async

调试插曲

正常调试,想看看 Spring 是如何处理循环依赖的;在
AbstractAutowireCapableBeanFactory#doCreateBean
的 606 行打个断点,同时给断点加个
Condition

断点condition

开始调试,为了方便查看三级缓存中的内容,我们添加三个
watch

添加watch

将三级缓存都添加进来

三级缓存watch

此时我们来看第二级缓存
earlySingletonObjects

二级缓存空的

是没有内容的,我们再看下第三级缓存

第三级缓存非空

circle 怎么会到第三级缓存中,跟循环依赖有关;接下来去看下第一级缓存,找到
loop

第一级缓存loop 点击toString

点一下
circle

toStrng()
,然后我们
F8
一下(代码 606 行执行完毕,来到 607 行,607行并未执行),再去看第二级缓存

第二级缓存非空_有circle

第二级缓存竟然有元素了,那第三级缓存的
circle
还存在吗

第三级缓存_circle没了

很显然,是有什么操作将第三级缓存中的
circle
提前曝光到第二级缓存了,回顾下这期间我们做了哪些操作?

  1. 点了 circle 的 toString()
  2. F8,执行了代码 606 行:if (earlySingletonExposure)

这就很明显了,肯定是点了 circle 的 toString() 导致的,怎么验证了?其实很简单,重新开始调试,来到 AbstractAutowireCapableBeanFactory 606 行后,啥也别动,直接在
DefaultSingletonBeanRegistry#getSingleton
182 行打个断点

DefaultSingletonBeanRegistry

然后再回到 AbstractAutowireCapableBeanFactory 606,再去第一级缓存中找 loop,然后点击它的 circle 的 toString,IDEA 会提示如下信息

调试计算断点忽略

Skipped breakpoint at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry:182 because it happened inside debugger evaluation
Troubleshooting guide

翻译过来就是

忽略 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry:182 的断点,因为它发生在调试器内部,详情请看
Troubleshooting guide

提前曝光就提前曝光呗,放开断点,程序能够正常执行完毕,有什么关系呢?那我就再给你们加点料,CircleTest.java 上加上
@EnableAsync

/**
 * @author: 青石路
 */
@ComponentScan(basePackages = "com.qsl")
@EnableAsync
public class CircleTest {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(CircleTest.class);
        Circle circle = ctx.getBean(Circle.class);
        Loop loop = ctx.getBean(Loop.class);
        System.out.println(circle.getLoop());
        System.out.println(loop);
    }
}

Circle.java 的 sayHello 方法上加上
@Async

/**
 * @author: 青石路
 */
@Component
public class Circle {

    @Autowired
    private Loop loop;

    public Loop getLoop() {
        return loop;
    }

    @Async
    public void sayHello(String name) {
        System.out.println("circle sayHello, " + name);
    }
}

重复之前的调试过程(记得去找第一级缓存中的
loop

circle
,然后点其
toString()
),取消所有断点后
F9

BeanCurrentlyInCreationException
它就来了

Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'circle': Bean with name 'circle' has been injected into other beans [loop] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:623)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
	at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:89)
	at com.qsl.CircleTest.main(CircleTest.java:16)

异常信息已经说的很清楚了

创建名为 circle 的bean时出错:注入给 loop bean 的是 circle 的代理实例,而非最终进入到第一级缓存的 circle bean

相当于注入给 loop bean 的是 circle 的代理对象实例,而提前曝光的是 circle 的半成品对象,两处不一致;究其原因还是我们操作 circle 的 toString,导致半成品对象提前曝光了

我们来梳理下
Circle

Loop
的实例创建过程。根据
Spring
的扫描规则,Circle 是被先扫描到的

三探循环依赖 → 记一次线上偶现的循环依赖问题
有介绍扫描规则

所以
Circle
实例会先被创建,因为
@Async
(底层实现:代理),第三级缓存提前创建 Circle 代理对象

circle代理对象存入三级缓存

接着填充 Circle 半成品对象的属性
Loop loop
,所以继续创建 Loop 实例,第三级缓存提前创建 Loop 代理对象(用不到,后续直接 remove)

Loop代理对象存入第三级缓存

此时我们看下当前线程的栈帧

创建loop时的栈帧

接着填充 Loop 半成品对象的属性
Circle circle
,此时 circle 还没创建完,所以填充给 loop 的 circle 肯定是第三级缓存中 circle 的代理对象

loop的circle属性

填充完后,loop 实例创建完毕,会添加到第一级缓存中,并移除第三级缓存中的 loop(呼应前面说到的:用不到,后续直接 remove)和第二级缓存中的 loop(没有)

loop实例加入第一级缓存

此时 loop 来到了第一级缓存,成为了
成品
实例,而 circle 还在第三级缓存中,第二级缓存仍是空;loop 实例创建好之后,回到 circle 的属性填充,将 loop 成品填充给半成品 circle

loop填充到circle中

初始化 circle 完成后,此时 circle 的曝光对象(exposedObject)是

circle曝光对象

此时已经到 606 行了,大家知道该做什么了吧,去第一级缓存中找到 loop,然后点击它的 circle 的
toString()

点击circle toString

然后我们进入
getSingleton
方法,此时 circle 在缓存中的位置发生了变化

circle来到第二级缓存

正是这个变化,导致了接下来的流程发生了变化;我们继续往下看,getSingleton 方法返回了二级缓存中的 circle,而非正常流程下的
null

circle_问题关键点

exposedObject
不等于
bean
,会来到 else if 分支判断是否有依赖 circle 的 bean,很显然有(loop),最后就来到异常分支

if (!actualDependentBeans.isEmpty()) {
	throw new BeanCurrentlyInCreationException(beanName,
			"Bean with name '" + beanName + "' has been injected into other beans [" +
			StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
			"] in its raw version as part of a circular reference, but has eventually been " +
			"wrapped. This means that said other beans do not use the final version of the " +
			"bean. This is often the result of over-eager type matching - consider using " +
			"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
}

凡是涉及到代理的,最终在第一级缓存中的都是实例的代理对象,比如 circle,我们取消掉所有断点,只在 CircleTest.java 上打一个断点,看看 circle 和 loop 实例就清楚了

circle是代理对象而loop不是

总结

  1. Spring 调试过程中不要随便去点代理对象的
    toString
    ,它可能会导致对象的提前曝光,打乱了 Spring bean 的创建过程,最终导致异常;抛异常倒是够直观,就怕不抛异常,然后运行过程中出现各种奇葩问题

  2. IDEA 调试配置


    debug对象toString默认调用开关

    有些版本默认是勾上的,这就会导致调试后过程中,我们去查看对象的时候自动调用对象的
    toString
    方法,可能引发一些异常,比如上文中介绍的循环依赖 circle 提前曝光的问题

  3. 实际工作中,大家基本遇不到文中的情况,看看图个乐就行


    20240128194820

大家好,我是码农先森。

守护进程顾名思义就是能够在后台一直运行的进程,不会霸占用户的会话终端,脱离了终端的控制。相信朋友们对这东西都不陌生了吧?如果连这个概念都还不能理解的话,建议回炉重造多看看 Linux 进程管理相关的基础知识。在我们日常的编程中常见有类似
php think ...

php artisan ...

php yii ...
等命令启动需要一直执行的任务,都会通过
nohup
挂载到后台保持长期运行的状态。同样在 Workerman 中也是使用类似
php index.php start
的命令来启动进程,但不同的是它不需要利用
nohup
便可以挂载到后台运行。那有些朋友就会好奇它是怎么实现的呢?为了解决朋友们的疑惑,我们今天就重点深入分析一下 Workerman 守护进程的实现原理。

我们先了解一些进程相关的知识:

  • 父进程:父进程是生成其他进程的进程。当一个进程创建了另一个进程时,创建者被称为父进程,而被创建的进程则成为子进程。父进程可以通过进程标识符(PID)来识别它所创建的子进程。
  • 子进程:子进程是由父进程创建的新进程。子进程继承了父进程的一些属性,例如环境变量、文件描述符等。子进程独立于父进程运行,它可以执行自己的代码,并且具有自己的资源和内存空间。
  • 进程组:进程组是一组相关联的进程的集合。每个进程组都有一个唯一的进程组ID(PGID),用于标识该进程组。进程组通常由一个父进程创建,并且包含了与父进程具有相同会话ID(SID)的所有子进程。
  • 会话:会话是一组关联进程的集合,通常由用户登录到系统开始,直至用户注销或关闭终端会话结束,一个会话中的进程共享相同的控制终端。每个会话都有一个唯一的会话ID(SID),用于标识该会话。会话通常包含一个或多个进程组,其中第一个进程组成为会话的主进程组。

这些概念俗称八股文,向来都不怎么好理解,那我们来看个例子。执行了命令
php index.php
便产生了进程
61052
「该进程的父进程是 Bash 进程 8243,这里不用管它」,然后通过 Fork 创建了子进程
61053
且其父进程就是
61052
,这两个进程拥有共同的进程组
61052
和会话
8243
。调用 posix_setsid 函数,将会为子进程
61053
开启新的进程组
61053
和新的会话
61053
,这里的会话可以理解为一个新的命令窗口终端。最后子进程
61053
通过 Fork 创建了子进程
61054
,进程
61053
升级成了父进程,这里再次 Fork 的原因是要避免被终端控制进程所关联,这个进程
61052
是在终端的模式下创建的,自此进程
61054
就形成了守护进程。

[manongsen@root phpwork]$ php index.php
[parent] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243 
[parent1] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243 退出了该进程
[child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61052, 会话ID: 8243 
[child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053 
[parent2] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053 退出了该进程
[child2] 进程ID: 61054, 父进程ID: 61053, 进程组ID: 61053, 会话ID: 61053 保留了该进程

[manongsen@root phpwork]$ ps aux | grep index.php
root             66064   0.0  0.0 408105040   1472 s080  S+   10:00下午   0:00.00 grep index.php
root             61054   0.0  0.0 438073488    280   ??  S    10:00下午   0:00.00 php index.php

上面举例的进程信息,正是这段代码运行所产生的。如果看了这段代码且细心的朋友,会发现为什么 posix_setsid 这个函数不放在第一次 Fork 前调用,而在第二次 Fork 前调用呢,这样的话就不用 Fork 两次了?原因是组长进程是不能创建会话的,进程组ID
61052
和进程ID
61052
相同「即当前进程则为组长进程」,所以需要子进程来创建新的会话,这一点需要特别注意一下。

<?php

function echoMsg($prefix, $suffix="") {
    // 进程ID
    $pid = getmypid(); 
    // 进程组ID
    $pgid = posix_getpgid($pid);
    // 会话ID
    $sid = posix_getsid($pid); 
    // 父进程ID
    $ppid = posix_getppid();

    echo "[{$prefix}] 进程ID: {$pid}, 父进程ID: {$ppid}, 进程组ID: {$pgid}, 会话ID: {$sid} {$suffix}" . PHP_EOL;
}

// [parent] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243
echoMsg("parent");

// 第一次 Fork 进程  
$pid = pcntl_fork();
if ( $pid < 0 ) {
    exit('fork error');
} else if( $pid > 0 ) {
    // [parent1] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243 退出了该进程
    echoMsg("parent1", "退出了该进程");
    exit;
}

// 创建的 子进程ID 为 61053 但 进程组、会话 还是和父进程是同一个
// [child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61052, 会话ID: 8243 
echoMsg("child1");

// 调用 posix_setsid 函数,会创建一个新的会话和进程组,并设置 进程组ID 和 会话ID 为该 进程ID
if (-1 === \posix_setsid()) {
    throw new Exception("Setsid fail");
}

// 现在会发现 进程组ID 和 会话ID 都变成了 61053 在这里相当于启动了一个类似 Linux 终端下的会话窗口
// [child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053 
echoMsg("child1");

// 第二次 Fork 进程
// 这里需要二次 Fork 进程的原因是避免被终端控制进程所关联,这个进程 61052 是在终端的模式下创建的
// 需要脱离这个进程 61052 以确保守护进程的稳定
$pid = pcntl_fork();
if ( $pid  < 0 ){
    exit('fork error');
} else if( $pid > 0 ) {
    // [parent2] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053 退出了该进程
    echoMsg("parent2", "退出了该进程");
    exit;
}

// 到这里该进程已经脱离了终端进程的控制,形成了守护进程
// [child2] 进程ID: 61054, 父进程ID: 61053, 进程组ID: 61053, 会话ID: 61053 保留了该进程
echoMsg("child2", "保留了该进程");

sleep(100);

有时间的朋友最好自行执行代码并分析一遍,会有不一样的收获。这里假装你已经实践过了,这下我们来看 Workerman 的 Worker.php 文件中 554 行的 runAll 方法中的 static::daemonize() 这个函数,实现的流程逻辑和上面的例子几乎一样。不过这里还使用了 umask 这个函数,其主要的作用是为该进程所创建的文件或目录赋予相应的权限,保证有权限操作文件或目录。

// workerman/Worker.php:554
/**
 * Run all worker instances.
 * 运行进程
 * @return void
 */
public static function runAll()
{
    static::checkSapiEnv();
    static::init();
    static::parseCommand();
    static::lock();
    // 创建进程并形成守护进程
    static::daemonize();
    static::initWorkers();
    static::installSignal();
    static::saveMasterPid();
    static::lock(\LOCK_UN);
    static::displayUI();
    static::forkWorkers();
    static::resetStd();
    static::monitorWorkers();
}

// workerman/Worker.php:1262
/**
 * Run as daemon mode.
 * 使用守护进程模式运行
 * @throws Exception
 */
protected static function daemonize()
{
	// 判断是否已经是守护状态、以及当前系统是否是 Linux 环境
    if (!static::$daemonize || static::$_OS !== \OS_TYPE_LINUX) {
        return;
    }
    
    // 设置 umask 为 0 则当前进程创建的文件权限都为 777 拥有最高权限
    \umask(0);
    
    // 第一次创建进程
    $pid = \pcntl_fork();
    if (-1 === $pid) {
    	// 创建进程失败
        throw new Exception('Fork fail');
    } elseif ($pid > 0) {
    	// 主进程退出
        exit(0);
    }

	// 子进程继续执行...
    // 调用 posix_setsid 函数,可以让进程脱离父进程,转变为守护进程
    if (-1 === \posix_setsid()) {
        throw new Exception("Setsid fail");
    }

	// 第二次创建进程,在基于 System V 的系统中,通过再次 Fork 父进程退出
	// 保证形成的守护进程,不会成为会话首进程,不会拥有控制终端
    $pid = \pcntl_fork();
    if (-1 === $pid) {
    	// 创建进程失败
        throw new Exception("Fork fail");
    } elseif (0 !== $pid) {
    	// 主进程退出
        exit(0);
    }

    // 子进程继续执行...
}

守护进程也是 Workerman 中重要的一部分,它保障了 Workerman 进程的稳定性。不像我们通过
nohup
启动的命令,挂起到后台之后,有时还神不知鬼不觉的就挂了,朋友们或许都有这样的经历吧。当然在市面上也有一些开源的守护进程管理软件,比如 supervisor 等,其次还有人利用会话终端 screen、tmux 等工具来实现。其实守护进程的实现方式有多种多样,我们这里只是为了分析 Workerman 中守护进程的实现原理,而引出了在 PHP 中实现守护进程模式的例子,希望本次的内容能对你有所帮助。

感谢大家阅读,个人观点仅供参考,欢迎在评论区发表不同观点。


欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。

Redis 一旦服务器宕机,内存中的数据将全部丢失,从后端数据库恢复这些数据,对数据库压力很大,且性能肯定比不上从 Redis 中读取,会拖慢应用程序。所以,对 Redis 来说,实现数据的
持久化
,避免从后端数据库中进行恢复,是至关重要的。

1、AOF 日志

AOF 日志是先执行命令,把数据写入内存,然后才记录日志以文本形式保存,如下图:"*3" 表示命令有三个部分组成,每部分由"$+数字"开头,"$3 set"表示这部分有三个字节,指"set"命令,"$7 testkey"表示该部分有七个字节,即"testkey"命令,以此类推。

AOF 写后日志只有命令能执行成功,才会被记录到日志中,避免额外的检查开销,也避免了出现记录错误命令的情况,而且不会阻塞当前的写操作。说完
优点

风险
,如果刚执行完命令还没有来得及记日志就宕机了,就有丢失的风险。其次,AOF 日志在主线程中执行,如果在把日志文件写入磁盘压力过大,可能会带来阻塞风险。


AOF 风险与写回磁盘有关,针对这个问题提供了三种
写回策略
,即配置项 appendfsync 的三个可选值:

(1)Always 同步写回:每个写命令执行完,立马同步地将日志写回磁盘
(2)Everysec 每秒写回:每个写命令执行完,先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘
(3)No 操作系统控制的写回:每个写命令执行完,先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘

三种策略各有优劣,汇总如下:

选定写回策略,并非万事大吉,随着接收的写命令越来越多,AOF 文件会越来越大,带来性能问题。主要是以下三个方面:
(1)文件系统本身对文件大小有限制,无法保存过大的文件
(2)如果文件太大,之后再往里面追加命令记录的话,效率也会变低
(3)如果发生宕机,AOF 中记录的命令要一个个被重新执行,文件太大导致整个恢复过程就会非常缓慢,影响 Redis 正常使用

日志文件太大了怎么办呢?这个时候,AOF
重写机制
就登场了。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令,而重写时,只会根据这个键值对当前的最新状态,为它生成对应的写入命令,这样一来,一个键值对在重写日志中只用一条命令就行了,并且在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了。举个栗子:


AOF 重写并不会阻塞主线程,重写过程是由后台线程 bgrewriteaof 来完成的,通过内存拷贝和两处日志保证数据的完整性。

2、RDB 内存快照

内存快照 RDB 就是 Redis DataBase 的缩写,和 AOF 相比,RDB 记录的是某一时刻的数据,并不是操作,所以在做数据恢复时,我们可以直接把 RDB 文件读入内存,很快地完成恢复。但同时也面临两个问题:
(1)对哪些数据做快照?这关系到快照的执行效率问题。
(2)做快照时,数据还能被增删改吗?这关系到 Redis 是否被阻塞,能否同时正常处理请求。

为了提供所有数据的可靠性保证,全量快照会把内存中的所有数据都记录到磁盘中,一个都不少。这样会花费很多时间,全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。对于 Redis 而言,它的单线程模型就决定了,我们要尽量避免所有会阻塞主线程的操作。Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave:
(1)save:在主线程中执行,会导致阻塞。
(2)bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。

bgsave 避免主线程阻塞,可以正常接收请求,但是,为了保证快照完整性,它只能处理读操作,不能修改正在执行快照的数据。Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。示意如图:

简单来说,主线程 fork 生成 bgsave 子进程,可共享主线程的所有内存数据。bgsave 子进程运行读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作(例如图中的键值对 A),则主线程和子进程互不影响。如果主线程要修改数据(例如图中的键值对 C),则会生成该数据的副本,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

至此上面提的两个问题“哪些数据做快照”、“做快照时数据能否修改”就都解决了。新的问题又产生了,快照间隔多久做一次合适?如果在第二次快照前宕机,就可能出现数据丢失的问题,如果太频繁又会出现第一个还没结束,第二个又开始的情况。虽然 bgsave 执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会给磁盘带来额外的开销,并且 bgsave 子进程需要通过 fork 操作从主线程创建出来,频繁操作依然会阻塞主线程。

此时,增量快照就登场了,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。比如 T1 和 T2 时刻如果再做快照,我们只需要将被修改的数据写入快照文件就行。

虽然我们记住哪些数据被修改了,但“记住”这个操作,需要我们使用额外的元数据信息去记录,这会带来额外的空间开销问题。有时改动较小时,又要引入的额外空间区记录,有些得不偿失。此时我们就可以混合使用 AOF 日志和内存快照的方法,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。

如图,T1 和 T2 时刻的修改,用 AOF 日志记录,在第二次做全量快照时,就可以清空 AOF 日志,因为修改都已经记录到快照中了。这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势,可谓鱼和熊掌兼得。

这次想到应用程序的窗体的快捷操作键的使用的问题。

上次发布过一个快捷键的例子(
https://www.cnblogs.com/lzhdim/p/18342051
),区别在于它是操作系统全局注册的热键,如果其它应用程序注册了对应的热键,那就会失效。此例子是对某个窗体里的按键的操作进行的快捷键的操作,有别于全局热键的效果,更加的符合应用程序的效果。

1、
项目目录;

2、
源码介绍;

1) 快捷键对象;

2) 快捷键对象定义;

3) 快捷键的实现;

4) 快捷键操作代码;

3、
运行界面;

4、
使用介绍;

1) 参考例子中的代码进行复用。

2) 在配置界面设置快捷键,在窗体上点击该快捷键。

5、
源码下载;

https://download.csdn.net/download/lzhdim/89621777

6、
其它建议;

这个例子抛砖引玉,其它的请读者自己去复用成需要的代码。

这个例子对于上次的例子区别是自定义的快捷键是对于某个窗体的操作,上次的例子是操作系统全局的热键注册。