Spring Cloud 项目练习-博客系统

目录

1. 项⽬介绍:

2. 系统设计:

3. 准备⼯作:

4. 环境和⼯程搭建:

配置Nacos:

⽹关服务:

⽤⼾认证过滤:

Nacos 配置MySQL:

配置URL⽩名单:

6. ⽤⼾注册功能:

引⼊Redis:

Redis介绍和简单使用:

@Conditional:

引⼊RabbitMQ:

RabbitMQ简单介绍参考:

RabbitMQ 的安装

SpringBoot 继承RabbitMQ

集成RabbitMQ:

引⼊邮件发送:

7. 前端开发&部署

Nignx简单介绍参考

后端允许跨域:

Linux部署

1. 项⽬介绍:

背景:

为了进⼀步加深对Spring Cloud组件的理解和应⽤,通过这个项⽬的练习, 加深对Spring Cloud的理解.

需求描述:

和Spring Boot阶段的博客系统类似, 除此之外, 增加⽤⼾注册和邮箱通知. 简单描述功能:

1. ⽤⼾功能

        a. ⽤⼾注册, 登录.

        b. ⽤⼾注册完成后, 发邮件通知.

        c. 查询⽤⼾信息

2. 博客功能

        a. 博客增删改查.

2. 系统设计:

服务拆分原则:

拆分微服务—般遵循如下原则:

1. 单⼀职责原则

单⼀职责原则原本是⾯向对象设计中的⼀个基本原则, 它指的是⼀个类应该专注于单⼀功能. 不要存在多于⼀个导致类变更的原因.

在微服务架构中, ⼀个微服务也应该只负责⼀个功能或业务领域, 每个服务应该有清晰的定义和边界, 只关注⾃⼰的特定业务领域.

2. 服务⾃治原则

服务⾃治是指每个微服务都应该具备⾼度⾃治的能⼒, 即每个服务要能做到独⽴开发, 独⽴测试, 独⽴构建, 独⽴部署, 独⽴运⾏. 不需要过多关注其他微服务的状态和数据, 有助于提⾼团队的协同开发效率和产品的交付速度.

3. 单向依赖原则

微服务之间需要做到单向依赖, 严禁循环依赖, 双向依赖

循环依赖: A -> B -> C ->A

双向依赖: A -> B, B -> A

如果⼀些场景确实⽆法避免循环依赖或者双向依赖, 可以考虑使⽤消息队列等其他⽅式来实现

微服务拆分⽅案:

微服务拆分没有绝对标准的⽅案, 服务拆分需要考虑维护成本, 系统的可扩展性, 软件的发布频率等. 最重要的是根据具体的业务场景和团队情况进⾏合理的规划和调整. 随着业务的发展, 原先的架构⽅案可能需要进⾏调整以适应新的需求.

根据前⼈的经验, 不同的技术专家也给出不同的拆分建议, 下⾯介绍⼏种常⻅的拆分⽅案:

1. 纵向拆分: 从业务维度进⾏拆分. 标准是按照业务的关联程度来决定, 关联⽐较密切的业务适合拆分为⼀个微服务,⽽功能相对⽐较独⽴的业务适合单独拆分为⼀个微服务.

2. 横向拆分: 从公共且独⽴功能维度拆分, 按照是否有公共的被多个其他服务调⽤,且依赖的资源独⽴不与其他业务耦合.

3. 基于稳定性拆分: 将系统中的业务模块按照稳定性进⾏排序. 稳定的, 不经常修改的划分⼀块. 将不稳定的, 经常修改的划分为⼀个独⽴服务. ⽐如⽇志服务, 监控服务都是相对稳定的服务, 可以归到⼀起. ⽐如20%经常变动的部分进⾏抽离, 80%不经常变动的单独部署和管理. 这样可以尽可能的减少发布频率. 减少发布产⽣的后遗症.

4. 基于可靠性拆分: 将系统中的业务模块按照可靠性进⾏排序, 对可靠性要求⽐较⾼的核⼼模块归在⼀起, 对可靠性要求不⾼的⾮核⼼模块归在⼀块. 避免"⼀颗⽼⿏屎坏了⼀锅粥"的单体弊端, 同时将来要做⾼可⽤⽅案也能很好的节省机器或带宽的成本.

5. 基于⾼性能拆分: 将系统中的业务模块按照对性能的要求进⾏优先级排序. 把对性能要求较⾼的模块独⽴成⼀个服务, 对性能要求不⾼的放在⼀起. ⽐如全⽂搜索, 商品查询独⽴成单独的微服务拆分出来.

6. .....

这些拆分从⼀些⻆度来看, 可能会冲突或者重合. 所以这⾥的拆分应该有个优先级, 在拆分相互冲突的时候应该要优先考虑权重⽐较⾼的那个(具体以业务场景来衡量)

💡 微服务架构并⽆标准架构, 合适的就是最好的, 不然架构师⼤会也不会各个系统架构百花⻬放了.

在架构设计的过程中, 坚持 "合适优于业界领先", 避免"过度设计"(为了设计⽽设计).

罗⻢不是⼀天建成的, 很多业界领先⽅案并不是⼀群天才在某个时期⼀下⼦做出来的, ⽽是经过数年的发展逐步完善. 业界领先的⽅案⼤多是"逼"出来的, 随着业务的发展, 量变导致质变, 新的问题出现了, 当前的⽅案⽆法满⾜需求, 需要⽤新的⽅案来解决. 通过不断的创新和尝试, 业界领先的⽅案才得以形成.

博客系统拆分:

业务优先是最基本, 最重要的划分⽅式. 博客系统的业务⽐较简单, 分为两⼤块: ⽤⼾模块, 博客模块.

我们把博客系统拆分为⽤⼾服务和博客服务, 对于公共且功能独⽴的模块, 抽取出公共SDK(复杂业务场景, 也可以抽取出公共服务模块, 此处选择SDK).

技术选型:

博客系统从⼤的模块分为展⽰层, ⽹关层, 服务层, 数据层.

展⽰层: 依然采⽤课堂中讲的HTML+CSS+JavaScript+JQuery, 使⽤Nginx来提供静态资源服务. ⽹关: 采⽤Spring Cloud Gateway

服务层: 采⽤SpringBoot框架进⾏开发, MyBatisPlus访问数据库, 服务拆分为⽤⼾服务和博客服务, 服务之间使⽤OpenFeign进⾏远程调⽤. Nacos来处理服务注册/服务发现以及配置相关的内容.

◦ 为了提⾼⽤⼾登录的性能, 使⽤Redis作为缓存.

◦ ⽤⼾注册成功后, 给⽤⼾发邮箱, 使⽤RabbitMQ来解耦

◦ 数据交互使⽤JSON, 为了提⾼⼤家知识的⼴度, 本次引⽤fastjson2

数据层: 采⽤MySQL来提供数据服务

公共SDK: 针对⼀些公共模块, 把代码抽取出来, ⽅便其他模块使⽤, ⽐如发送邮件, 操作Redis, Jwt, Json处理等.

基于以上, 博客系统的系统架构简单如下:

3.准备⼯作:

前置代码:

代码: Spring Cloud版本博客系统是基于SpringBoot版本进⾏开发改造的,由SpringBoot 版本代码进⾏改造.

⻚⾯: 新增两个⻚⾯: 1. 注册⻚⾯ 2. 注册成功⻚⾯

数据设计:

数据库⼤多数和SpringBoot版本保持⼀致, 修改部分如下:

1. ⽤⼾表: 新增邮箱字段(⽤⼾注册需要)

2. 数据表拆分为不同的数据库

数据库初始化SQL:

-- 用户服务数据库
create database if not exists spring_cloud_user charset utf8mb4;

use spring_cloud_user;
-- 用户表
DROP TABLE IF EXISTS spring_cloud_user.user_info;
CREATE TABLE spring_cloud_user.user_info(
        `id` INT NOT NULL AUTO_INCREMENT,
        `user_name` VARCHAR ( 128 ) NOT NULL,
        `password` VARCHAR ( 128 ) NOT NULL,
        `github_url` VARCHAR ( 128 ) NULL,
        `email` VARCHAR ( 128 ) NULL,
        `delete_flag` TINYINT ( 4 ) NULL DEFAULT 0,
        `create_time` DATETIME DEFAULT now(),
        `update_time` DATETIME DEFAULT now() ON UPDATE now(),
        PRIMARY KEY ( id ),
UNIQUE INDEX user_name_UNIQUE (user_name ASC )) ENGINE = INNODB DEFAULT CHARACTER 
SET = utf8mb4 COMMENT = '用户表';

-- 新增用户信息
insert into spring_cloud_user.user_info (user_name, password,github_url)values("zhangsan","11eb2423c6064044aaa1df7ed6156331d1244f113ad54a982cb0215e0af22b68","https://gitee.com/bubble-fish666");
insert into spring_cloud_user.user_info (user_name, password,github_url)values("lisi","25ce8e32469c4bdb9868c182b6164d3006de2cb47383384df8279e7cfc251d9d","https://gitee.com/bubble-fish666");


-- 博客服务数据库
create database if not exists spring_cloud_blog charset utf8mb4;

use spring_cloud_blog;

-- 博客表
drop table if exists spring_cloud_blog.blog_info;
CREATE TABLE spring_cloud_blog.blog_info (
  `id` INT NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(200) NULL,
  `content` TEXT NULL,
  `user_id` INT(11) NULL,
  `delete_flag` TINYINT(4) NULL DEFAULT 0,
  `create_time` DATETIME DEFAULT now(),
  `update_time` DATETIME DEFAULT now() ON UPDATE now(),
  PRIMARY KEY (id))
ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '博客表';



insert into spring_cloud_blog.blog_info (title,content,user_id) values("第一篇博客","111我是博客正文我是博客正文我是博客正文",1);
insert into spring_cloud_blog.blog_info (title,content,user_id) values("第二篇博客","222我是博客正文我是博客正文我是博客正文",2);

4.环境和⼯程搭建:

⼯程结构介绍

整个项⽬共创建3个服务(⽹关服务, 博客服务, ⽤⼾服务), ⼀个公共SDK, 为了⽅便代码管理, 我们采⽤⽗⼦⼯程的⽅式进⾏创建(也可以分开创建单独的SpringBoot项⽬).

整体结构如下:

spring-cloud-blog
├── .idea
├── blog-common (公共SDK)
│   └── src
│       └── main
│           └── java
│               └── com.bite.common (公共工具类、枚举、异常等)
├── blog-info (博客服务父模块)
│   ├── blog-info-api (博客服务API模块)
│   │   └── src
│   │       └── main
│   │           └── java
│   │               └── com.bite.blog.api (API接口、请求/响应实体)
│   ├── blog-info-service (博客服务实现模块)
│   │   └── src
│   │       └── main
│   │           ├── java
│   │           │   └── com.bite.blog (Controller、Service、Mapper等)
│   │           └── resources (配置文件)
│   └── pom.xml
├── gateway-service (网关服务)
│   └── src
│       └── main
│           ├── java
│           │   └── com.bite.gateway (启动类、过滤器等)
│           └── resources (配置文件)
├── user-info (用户服务父模块)
│   ├── user-info-api (用户服务API模块)
│   │   └── src
│   │       └── main
│   │           └── java
│   │               └── com.bite.user.api (API接口、请求/响应实体)
│   ├── user-info-service (用户服务实现模块)
│   │   └── src
│   │       └── main
│   │           ├── java
│   │           │   └── com.bite.user (Controller、Service、Mapper等)
│   │           └── resources (配置文件)
│   └── pom.xml
├── pom.xml (父工程pom)
└── spring-blog.log

创建⼯程:

创建⽗⼯程

1. 创建⼀个空的Maven项⽬, 删除所有代码, 只保留pom.xml

⽬录结构:

spring-cloud-blog
├── .idea
└── pom.xml

2. 完善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>org.example</groupId>
    <artifactId>spring-cloud-blog</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>blog-info</module>
        <module>user-info</module>
        <module>gateway-service</module>
        <module>blog-common</module>
    </modules>

    <!-- 声明父工程的打包方式为pom -->
    <packaging>pom</packaging>

    <!-- 完善依赖 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <java.version>17</java.version>
        <mybatis.version>3.0.3</mybatis.version>
        <mysql.version>8.0.33</mysql.version>
        <mysql.plus.version>3.5.5</mysql.plus.version>
        <spring-cloud.version>2022.0.3</spring-cloud.version>
        <spring-cloud-alibaba.version>2022.0.0.0-RC2</spring-cloud-alibaba.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
                <version>${mysql.plus.version}</version>
            </dependency>

            <dependency>
                <groupId>com.mysql</groupId>
                <artifactId>mysql-connector-j</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter-test</artifactId>
                <version>${mybatis.version}</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

创建⼦⼯程-博客服务&⽤⼾服务

博客服务有对外提供的接⼝, 所以拆分为api和service两个⼯程.

1. 创建⼀个空的⼯程, 只保留pom⽂件

2. 依次创建blog-info-api, blog-info-service

3.在service⼯程中添加⼯程基础依赖, 其他依赖随开发时再进⾏添加即可.

    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </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>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.5</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!-- SpringCloud 2020.*之后版本需要引入bootstrap-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
    </dependencies>

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

4. ⽤⼾服务同样的创建⽅式, 也可以采⽤复制的⽅式创建

创建⽹关服务:

⽹关服务作为系统的统⼀⼊⼝点, 通常情况下处理请求的转发, 认证授权, 限流等功能. 不需要拆分出api和service

重复上⾯的步骤, 创建⽹关服务gataway-service

添加⽹关依赖

<!--⽹关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

创建公共SDK:

创建⽅式和上述⼀样, 这个属于公共SDK包, 不需要添加SpringWeb相关依赖, 等开发相关公共功能处理时再添加对应依赖即可.

Spring Boot代码迁移:

SpringBoot代码简单分为以下⼏⼤块, 把相应代码挪到指定的⽬录下即可.

1. 博客相关代码

2. ⽤⼾相关代码

3. 公共代码

4. 前端代码(暂不处理)

1.公共代码:

把博客功能和⽤⼾功能都可能⽤到的代码挪到blog-common模块

有依赖相关报错, 添加响应依赖即可

    <dependencies>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </dependency>

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

2.⽤⼾系统:

1. 创建⽬录 com.jqq.user

2. 创建启动类UserServiceApplication

@EnableFeignClients(clients = {BlogServiceApi.class})
@SpringBootApplication
public class UserServiceApplication {
	public static void main(String[] args) {
		SpringApplication.run(UserServiceApplication.class, args);
	}
}

3. 调整博客系统相关代码

主要是以下⼏点:

• 请求响应相关代码, 放在api模块

添加相应依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>blog-common</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

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

• 编写OpenFeign API

@FeignClient(value = "user-service",path = "/user")
public interface UserServiceApi {

    @RequestMapping("/login")
    Result<UserLoginResponse> login(@RequestBody UserInfoRequest user);

    @RequestMapping("/getUserInfo")
    Result<UserInfoResponse> getUserInfo(@RequestParam("userId") Integer userId);

    @RequestMapping("/getAuthorInfo")
    Result<UserInfoResponse> getAuthorInfo(@RequestParam("blogId") Integer blogId);
}

实现UserServiceApi 接⼝, 添加 @Override 注解 • 修改启动类

• 拦截器放在⽹关处理(后⾯调整)

• 端⼝号和数据库配置

server:
  port: 9090
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring_cloud_user?characterEncoding=utf8&useSSL=false
    username: root
    password: "123456"
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
  file:
    name: user.log

3.博客系统:

1. 创建⽬录 com.jqq.blog

2. 创建启动类 BlogServiceApplication

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

3. 调整博客系统相关代码        

和⽤⼾系统类似

• 请求响应相关代码, 放在api模块

• 端⼝号和数据库配置

server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring_cloud_blog?characterEncoding=utf8&useSSL=false
    username: root
    password: "123456"
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
  file:
    name: blog.log

• 修改⽤⼾系统调⽤博客系统的⽅式, 服务间的调⽤通过OpenFeign完成.

SpringBoot阶段学习的 @ControllerAdvice 会改变接⼝的返回结果, 造成调⽤失败, 所以不能使⽤. 把Controller 返回结果改为 Result<?>

4.配置Nacos:

OpenFeign通过服务名来获取服务列表, 所以需要我们把博客系统和⽤⼾系统注册到Nacos上.

添加依赖和配置:

依赖:

        <!--Nacos依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

配置:

spring:
  application:
    name: blog-service
  cloud:
    loadbalancer:
      nacos:
        enabled: true
    nacos:
      discovery:
        server-addr: 152.136.172.144:10020

启动服务和Nacos:

启动Nacos, 启动服务, 进⾏测试, 观察博客系统和⽤⼾系统是否成功注册到Nacos上了

测试远程接⼝调⽤

http://127.0.0.1:8080/user/getAuthorInfo?blogId=1

5. ⽹关服务:

初始化项⽬:

1. 添加依赖

        <!--⽹关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--Nacos依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

2. 创建启动类

创建⽬录 com.jqq.gateway

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

3. 配置yml

server:
  port: 10030
spring:
  application:
    name: gateway-service
  cloud:
    nacos:
      discovery:
        server-addr: 152.136.172.144:10020
    gateway:
      routes: #⽹关路由配置
        - id: user-service #路由ID, ⾃定义, 唯⼀即可
          uri: lb://user-service #⽬标服务地址
          predicates: #路由条件
            - Path=/user/**
        - id: blog-service
          uri: lb://blog-service
          predicates:
            - Path=/blog/**

4. 测试⽹关

通过⽹关访问⽤⼾系统/博客系统, 确认响应结果

http://127.0.0.1:10030/user/getAuthorInfo?blogId=1

⽤⼾认证过滤:

⽤⼾登录后, 会返回前端token, ⾥⾯包含⽤⼾信息, 后续⽤⼾再访问时, 携带token信息, 以便系统验证⽤⼾⾝份, 这项⼯作, 我们放在过滤器中实现

1. 判断当前url是否拦截

2. 如果拦截, 验证⽤⼾⾝份

代码如下:

@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Autowired
    private ObjectMapper objectMapper;

    private List<String> whiteNames = List.of("/user/login","user/register");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //配置白名单,如果请求url为白名单,则放行
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        if(whiteNames.contains(path)){
            //放行
            return chain.filter(exchange);
        }
        //从header中获取token
        String userToken = request.getHeaders().getFirst("user_token");
        log.info("从header中获取token:{}",userToken);
        if(!StringUtils.hasLength(userToken)){
            //TODO
            return unauthorizedResponse(exchange,"令牌不能为空");
        }
        //验证用户token
        Claims claims = JWTUtils.parseJWT(userToken);
        if (claims==null){
            log.info("令牌验证失败");
            //TODO
            return unauthorizedResponse(exchange,"令牌过期或者违法");
        }
        return chain.filter(exchange);
    }

    @SneakyThrows
    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange , String errorMsg){
        log.error("[用户认证失败],url: {}", exchange.getRequest().getURI().getPath());
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        Result result = Result.fail(errorMsg);
        DataBuffer dataBuffer = response.bufferFactory().wrap(objectMapper.writeValueAsBytes(result));
        return response.writeWith(Mono.just(dataBuffer));
    }

    @Override
    public int getOrder() {
        return -200;//order值越小,优先级越高
    }
}

1. response.bufferFactory() 获取⼀个 BufferFactory 实例,这个实例⽤于创建和管理数据缓冲区, wrap ⽅法将这个字节数组包装成⼀个 DataBuffer 对象, DataBuffer 是WebFlux中⽤于处理数据流的接⼝.

2. Mono 是响应式编程库Reactor中的⼀个类, 代表⼀个异步的, 可能只发出⼀个数据的响应式类型, Mono.just(dataBuffer) 创建了⼀个包含 dataBuffer 的 Mono 实例,这意味着它将异步地发出⼀个数据,即前⾯创建的 DataBuffer 对象, response.writeWith(...)⽅法接受⼀个 Publisher ( Mono 是 Publisher 的⼀个实现),并将其作为HTTP响应的主体。 writeWith ⽅法将 Mono.just(dataBuffer) 作为响应体,发送给客⼾端.

3. Spring WebFlux 是 Spring 5 中引⼊的⼀个响应式编程模块, 旨在为构建⾮阻塞, 事件驱动的服务提供⽀持. Spring Cloud Gateway 是基于 Spring WebFlux 构建的, 这意味着 Spring Cloud Gateway 继承了 Spring WebFlux 的⾮阻塞 I/O 和反应式编程特性, 使其能够以异步的⽅式处理⼤量的并发请求,同时保持较低的资源消耗. 这种结合使得 Spring Cloud Gateway 能够⾼效地处理⾼并发请求,同时保持系统的可伸缩性和弹性.

Nacos 配置MySQL:

考虑到url⽩名单可能会被修改, 我们可以把url⽩名单放在配置⽂件中, 使⽤Nacos的配置功能来管理. 为了简化部署和测试, Nacos 默认情况下使⽤嵌⼊式数据库 Derby 来存储数据. 在⽣产环境中,为了提⾼性能和保证数据的持久化, 推荐使⽤ MySQL 或其他⽀持的关系型数据库, 如 PostgreSQL, Oracle等.

使⽤MySQL作为Nacos数据源的优势有以下⼏点:

1. 数据持久性: 数据不会因为 Nacos 服务的重启⽽丢失

2. ⾼性能:相⽐于内嵌数据库, MySQL 可以提供更⾼的读写性能和更好的并发处理能⼒.

3. 可扩展性:MySQL ⽀持集群部署, 可以⽔平扩展以满⾜⼤规模服务注册和服务发现的需求.

4. ⾼可⽤性:通过 MySQL 的⾼可⽤性解决⽅案, 如主从复制、集群等,可以提⾼数据的可靠性.

5. 易于维护:MySQL 拥有成熟的维护和管理⼯具,便于数据库的监控、备份和恢复。

接下来我们看下如何配置Nacos数据库 具体操作参考官⽅回复:nacos配置mysql数据库

主要分为以下⼏步:

1. 修改配置

2. 初始化数据库

3. 启动Nacos服务

修改配置

打开Nacos配置⽂件 conf/application.properties ,进⾏以下配置更改:

spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://你的数据库地址:端⼝/nacos?
characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=tru
e
db.user=你的数据库⽤⼾名
db.password=你的数据库密码

根据实际情况替换 你的数据库地址 、 端⼝ 、 nacos (数据库名)、 你的数据库⽤⼾名 和 你的数据库密码

初始化数据库

从Nacos的 conf ⽬录下找到 mysql-schema.sql (针对Nacos 2.x版本),并使⽤该SQL脚本在你的MySQL数据库中创建所需的表结构。执⾏脚本前,请确保你连接的是正确的数据库实例

-- 创建数据库
create if not exists nacos_config charset utf8mb4;
-- 执⾏SQL
SOURCE /usr/local/src/nacos/conf/mysql-schema.sql;

/usr/local/src/nacos/conf/mysql-schema.sql 改为你的真实⽬录

启动Nacos服务

完成上述配置后,重新启动Nacos服务. Nacos将使⽤配置的MySQL数据库作为其数据存储.

配置URL⽩名单:

接下来把URL⽩名单修改为从Nacos平台读取

代码改造

@Data
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "auth.white")
public class AuthWhiteName {
    private List<String> url;
}

在AuthFilter中注⼊⽩名单

@Autowired
    private AuthWhiteName authWhiteName;

修改路径排除逻辑

if(match(path,authWhiteName.getUrl())){
            //放行
            return chain.filter(exchange);
        }

private boolean match(String path, List<String> url) {
        if(url == null||url.size() == 0){
            return false;
        }
        return url.contains(path);
    }

添加配置:

添加依赖

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!-- SpringCloud 2020.*之后版本需要引⼊bootstrap-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

添加配置bootstrap.yml

spring:
  application:
    name: gateway-service
  cloud:
    nacos:
      config:
        server-addr: 152.136.172.144:10020
        file-extension: yaml

nacos配置:

创建配置 gateway-service.yaml 配置如下:

auth:
    white:
        url:
            - /user/login
            - /user/register

Data Id 格式介绍

在 Nacos Spring Cloud 中, dataId 的完整格式如下:

${prefix}-${spring.profiles.active}.${file-extension}

• prefix 默认为 spring.application.name 的值, 也可以通过配置项spring.cloud.nacos.config.prefix 来配置.

• spring.profiles.active 即为当前环境对应的 profile. 当spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}

• file-exetension 为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension 来配置。⽬前只⽀持 properties和 yaml 类型. 默认为properties.

微服务启动时, 会从Nacos读取多个配置⽂件:

${prefix}-${spring.profiles.active}.${file-extension} 如: productservice-dev.properties

${prefix}.${file-extension} , 如: product-service.properties

${prefix} 如product-service

配置之后, 可以查看到配置的内容在数据库中存储

重启服务, 验证

1. 登录时, 没有token, 可以正常登录

2. 获取⽤⼾信息时

• 没有token, 访问失败

• 存在token, 访问成功

http://127.0.0.1:10030/user/getAuthorInfo?blogId=1

6. ⽤⼾注册功能:

为了加深⼤家对于Redis和RabbitMQ的理解和应⽤, 我们在⽤⼾注册功能中添加Redis和RabbitMQ的使⽤.

我们先完成基本的⽤⼾注册功能, 再添加组件的使⽤.

业务开发:

接⼝定义

[请求]
/user/register
[参数]
contentType: application/json
{
    "userName":"wangwu",
    "password":"456789",
    "githubUrl": "https://gitee.com/bubble-fish666/spring-cloud",
    "email": "bite666@126.com"
} 
[响应]
{
    "code": 200,
    "errMsg": null,
    "data": 3 //⽤⼾ID
}

数据库表修改

⽤⼾注册增加邮箱字段(数据设计阶段, 提供的SQL已包含该部分内容)

后端开发

API定义

@Data
public class UserInfoRegisterRequest {
    @NotBlank(message = "用户名不能为空")
    @Length(max = 20,message = "用户名长度不能超过20")
    private String userName;
    @NotBlank(message = "密码不能为空")
    @Length(max = 20,message = "密码长度不能超过20")
    private String password;
    @Length(max = 64,message = "githubUrl长度不能超过64")
    private String githubUrl;
    @NotBlank(message = "邮箱不能为空")
    @Length(max = 20,message = "邮箱长度不能超过20")
    private String email;
}
@RequestMapping("/register")
    Result<Integer> register(@RequestBody UserInfoRegisterRequest registerRequest);

API实现

UserInfo 增加邮箱字段

Controller

@Override
    public Result<Integer> register(@Validated @RequestBody UserInfoRegisterRequest registerRequest) {
        return Result.success(userService.register(registerRequest));
    }

对应Service,Service实现

    @Override
    public Integer register(UserInfoRegisterRequest registerRequest) {
        //校验
        checkUserInfo(registerRequest);
        //用户注册,插入数据库
        UserInfo userInfo = BeanConvert.convertUserInfoEncrypt(registerRequest);
        try{
            int result = userInfoMapper.insert(userInfo);
            if(result == 1){
                return userInfo.getId();
            }else{
                throw new BlogException("用户注册失败");
            }
        }catch (Exception e){
            log.error("用户注册失败,e:{}",e);
            throw new BlogException("用户注册失败");
        }
    }

    private void checkUserInfo(UserInfoRegisterRequest param) {
        //用户名不能重复
        UserInfo userInfo = selectUserInfoByName(param.getUserName());
        if(userInfo!=null){
            throw new BlogException("用户名已存在");
        }
        //邮箱格式
        if(!RegexUtil.checkMail(param.getEmail())){
            throw new BlogException("邮箱格式不合法");
        }
        //url格式
        if(!RegexUtil.checkURL(param.getGithubUrl())){
            throw new BlogException("GithubUrl格式不合法");
        }
    }

邮箱和URL验证

邮箱和URL我们采⽤正则来进⾏校验.

githubURL并不校验地址为github的地址, 只校验位url即可


public class RegexUtil {
    /**
     * 匹配 邮箱:xxx@xx.xxx(形如:abc@qq.com)
     * ^ 表示匹配字符串的开始。
     * [a-z0-9]+ 表示匹配一个或多个小写字母或数字。
     * ([._\\-]*[a-z0-9])* 表示匹配零次或多次下述模式:一个点、下划线、反斜杠或短横线,后面跟着一个或多个小写字母或数字。这部分是可选的,并且可以重复出现。
     * @ 字符字面量,表示电子邮件地址中必须包含的"@"符号。
     * ([a-z0-9]+[-a-z0-9]*[a-z0-9]+.) 表示匹配一个或多个小写字母或数字,后面可以跟着零个或多个短横线或小写字母和数字,然后是一个小写字母或数字,最后是一个点。这是匹配域名的一部分。
     * {1,63} 表示前面的模式重复1到63次,这是对顶级域名长度的限制。
     * [a-z0-9]+ 表示匹配一个或多个小写字母或数字,这是顶级域名的开始部分。
     * $ 表示匹配字符串的结束。
     */
    private static final String emailRegex = "^[a-z0-9]+([._\\\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$";
    /**
     * 进行简单校验, 为URL即可
     * https://gitee.com/bubble-fish666/
     * ^:匹配字符串的开始。
     * (https?):匹配http或https。
     * :\/\/:匹配://。
     * ([a-zA-Z0-9.-]+):匹配域名,可以包含字母、数字、点和破折号。
     * (:\d+)?:可选部分,匹配冒号和端口号。
     * (\/[^\s]*)?:可选部分,匹配斜杠和路径,路径中不包含空白字符。
     * (\?[^\s]*)?:可选部分,匹配查询参数,参数中不包含空白字符。
     * $:匹配字符串的结束。
     */
    private static final String urlRegex = "^(https?):\\/\\/([a-zA-Z0-9.-]+)(:\\d+)?(\\/[^\\s]*)?(\\?[^\\s]*)?$";

    /**
     * 邮箱:xxx@xx.xxx(形如:abc@qq.com)
     *
     * @param content
     * @return
     */

    public static boolean checkMail(String content) {
        if (!StringUtils.hasText(content)) {
            return false;
        }
        return Pattern.matches(emailRegex, content);
    }

    public static boolean checkURL(String content) {
        if (!StringUtils.hasText(content)) {
            return false;
        }
        return Pattern.matches(urlRegex, content);
    }

    public static void main(String[] args) {
        System.out.println(checkMail("bite@126.com"));
    }
}
public class BeanConvert {
    public static UserInfo convertUserInfoEncrypt(UserInfoRegisterRequest registerRequest){
        UserInfo userInfo = new UserInfo();
        userInfo.setUserName(registerRequest.getUserName());
        userInfo.setPassword(SecurityUtil.encrypt(registerRequest.getPassword()));
        userInfo.setGithubUrl(registerRequest.getGithubUrl());
        userInfo.setEmail(registerRequest.getEmail());
        return userInfo;
    }
}

引⼊Redis:

Redis介绍和简单使用:

在对于性能要求⽐较⾼的模块中, Redis作为⼀个缓存层, 经常存储⼀些热点数据, 减少数据库的读取压⼒, 提⾼数据访问速度. 为了加深⼤家对Redis的理解和应⽤, 我们在博客系统的⽤⼾管理模块也加⼊Redis的使⽤.

Redis简单介绍参考:

1. Redis 是什么

Redis 是⼀种基于键值对(key-value)的 NoSQL 数据库,与很多键值对数据库不同的是,Redis 中的值可以是由 string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、HyperLogLog、GEO(地理信息定位)等多种数据结构和算法组成,因此 Redis 可以满⾜很多的应⽤场景. 因为 Redis 会将所有数据都存放再内存中,所以它的读写性能⾮常惊⼈。不仅如此,Redis 还可以将内存的数据利⽤快照和⽇志的形式保存到硬盘上,这样在发⽣类似断电或者机器故障的时候,内存中的数据不会“丢失”。除了上述功能以外,Redis 还提供了键过期、发布订阅、事务、流⽔线、Lua 脚本等附加功能。总之,如果在合适的场景使⽤号 Redis,它就会像⼀把瑞⼠军⼑⼀样所向披靡

2. Redis 的使⽤场景

缓存(Cache)

缓存机制⼏乎在所有⼤型⽹站都有使⽤,合理地使⽤缓存不仅可以加速数据的访问速度,⽽且能够有效地降低后端数据源的压⼒。Redis 提供了键值过期时间设置,并且也提供了灵活控制最⼤内存和内存溢出后的淘汰策略。可以这么说,⼀个合理的缓存设计能够为⼀个⽹站的稳定保驾护航。

排⾏榜系统

排⾏榜系统⼏乎存在于所有的⽹站,例如按照热度排名的排⾏榜,按照发布时间的排⾏榜,按照各种复杂维度计算出的排⾏榜,Redis 提供了列表和有序集合的结构,合理地使⽤这些数据结构可以很⽅便地构建各种排⾏榜系统。

计数器应⽤

计数器在⽹站中的作⽤⾄关重要,例如视频⽹站有播放数、电商⽹站有浏览数,为了保证数据的实时性,每⼀次播放和浏览都要做加 1 的操作,如果并发量很⼤对于传统关系型数据的性能是⼀种挑战。Redis 天然⽀持计数功能⽽且计数的性能也⾮常好,可以说是计数器系统的重要选择。

社交⽹络

赞 / 踩、粉丝、共同好友 / 喜好、推送、下拉刷新等是社交⽹站的必备功能,由于社交⽹站访问量通常⽐较⼤,⽽且传统的关系型数据不太合适保存这种类型的数据,Redis 提供的数据结构可以相对⽐较容易地实现这些功能。

消息队列系统

消息队列系统可以说是⼀个⼤型⽹站的必备基础组件,因为其具有业务解耦、⾮实时业务削峰等特性。Redis 提供了发布订阅功能和阻塞队列的功能,虽然和专业的消息队列⽐还不够⾜够强⼤,但是对于⼀般的消息队列功能基本可以满⾜。

3. Redis 的基本操作

Redis安装:

Ubuntu 安装Redis

#使⽤apt安装Redis

apt install redis -y

Redis 启动/停⽌

#查看Redis状态
systemctl status redis-server
#启动redis
service redis-server start
#停⽌redis服务
service redis-server stop
#重启redis服务
service redis-server restart

操作 Redis

使⽤以下命令启动redis客⼾端︰

redis-cli

常⻅命令介绍:

• SET key value

• GET key

• DEL key [key ...]

• EXISTS key [key ...]

• EXPIRE key seconds

• KEYS pattern(性能比较低,慎用)

更多命令:https://redis.io/commands/set/

4. Redis使⽤

SpringBoot集成Redis

添加依赖

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

或者使⽤快速⾯板添加Redis依赖

配置Redis

spring:
    data:
        redis:
            host: 127.0.0.1
            port: 6379
            timeout: 60s #连接空闲超过N(s秒、ms毫秒)后关闭,0为禁⽤,这⾥配置值和tcpkeepalive值⼀致
            lettuce:
                pool:
                    max-active: 8 #允许最⼤连接数
                    max-idle: 8 #最⼤空闲连接数, 默认8
                    min-idle: 0 #最⼩空闲连接数
                    max-wait: 5s #请求获取连接等待时间

测试代码:

@SpringBootTest
public class RedisTest {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Test
    void test(){
        redisTemplate.opsForValue().set("hello","spring");
        String hello = redisTemplate.opsForValue().get("hello");
        System.out.println(hello);
    }
}
Redis配置端⼝转发

Redis 服务器安装在云服务器上, 我们的代码在本地主机.

要想让本地主机能访问 redis, 有两种⽅式:

1. 开放Redis端⼝号[不推荐]

把 redis 的端⼝通过云服务器后台⻚⾯的 "防⽕墙" / "安全组" 放开端⼝到公⽹上, 但是这个操作⾮常危险(⿊客会顺着 redis 端⼝进来).

2. 端⼝转发[推荐],隧道

我们可以使⽤端⼝转发的⽅式, 直接把服务器的 redis 端⼝映射到本地.

具体操作:

在 xshell 中, 进⾏如下配置:

1) 右键云服务器的会话, 选择属性.

2) 找到隧道 -> 配置转移规则

3) 使⽤该会话连接服务器.

全部配置之后, 需要重新关闭连接, 打开连接才可以

后续访问云服务器的Redis, 就可以使⽤ 127.0.0.1 6379端⼝号(上图中配置的端⼝号) 来访问

Redis客⼾端:

除了通过云服务器查看Redis, 市⾯上也有⼀些客⼾端或者插件可以操作Redis 这两种⽅式分别各介绍⼀种

1. 客⼾端 Another-Redis-Desktop-Manager

下载地址: Another Redis Desktop Manager

安装后创建连接即可

端⼝号设置为上⾯配置的监听端⼝号即可

可以查看到刚才存储的数据

2. 插件

在File->Settings->Plugins->搜索Redis Helper插件

然后点击左上⻆+,登录远程Redis

在⽤⼾登录后, 可以将⽤⼾的信息存储在Redis中, 当需要获取⽤⼾信息时, 可以直接从Redis中获取, ⽽不需要每次都查询数据库, 这样可以加快数据的读取速度, 减少数据库的压⼒.

对于⽤⼾信息的增删改查, 除了数据库中存储, 也在Redis中进⾏存储, 简单流程如下:

本案例中, 我们只实现⽤⼾的注册和查询功能 接下来先看⽤⼾的注册如何实现.

针对Redis的操作, 我们可以封装⼀个⼯具包, 给所有模块使⽤

@Slf4j
public class Redis {
    private StringRedisTemplate redisTemplate;

    public Redis(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * redis的get操作
     */
    public String get(String key){
        try{
            return key == null ? null : redisTemplate.opsForValue().get(key);
        }catch (Exception e){
            log.error("redis get error,key:{}",key);
            return null;
        }
    }
    /**
     * hasKey
     * true则存在key
     */
    public Boolean hasKey(String key){
        try{
            return key == null ? false : redisTemplate.hasKey(key);
        }catch (Exception e){
            log.error("redis hasKey error,key:{}",key);
            return false;
        }
    }

    /**
     * set
     */
    public Boolean set(String key,String value){
        try{
            //可以增加参数校验
            redisTemplate.opsForValue().set(key,value);
            return true;
        }catch (Exception e){
            log.error("redis set error,key:{},value:{}",key,value);
            return false;
        }
    }

    /**
     * set 设置过期时间,单位为秒
     */
    public Boolean set(String key,String value,long timeout){
        try{
            //可以增加参数校验
            if(timeout>0){
                redisTemplate.opsForValue().set(key,value,timeout,TimeUnit.SECONDS);
            }else{
                set(key,value);
            }
            return true;
        }catch (Exception e){
            log.error("redis set error,key:{},value:{}",key,value);
            return false;
        }
    }
}

在使⽤时, 可以创建⼀个Redis对象, 调⽤Redis⾥的⽅法

@Configuration
public class RedisConfig {
    @Bean
    public Redis redis(StringRedisTemplate redisTemplate){
        return new Redis(redisTemplate);
    }
}

考虑到其他项⽬也可能会操作Redis, 我们可以把RedisConfig 放置在 jqq-common 包下.

思考:

问题⼀: 放在jqq-common包下的话, 并不在项⽬的扫描路径下, @Configuration 注解并不⽣效, 我们可以借助Spring的⾃动配置来让RedisConfig 交给Spring 管理.

问题⼆: 如果交给Spring的⾃动配置来完成RedisConfig 的管理, 那所有引⽤bite-common包的⼯程都会加载这个Bean, 但是我们的需求并不是如此.

解决办法:

借助 @Conditional 注解或扩展注解来实现根据条件完成Bean的配置管理.

@Conditional:

@Conditional 是 Spring 框架从 4.0 版本开始引⼊的⼀个注解,它允许开发者根据特定的条件来控制 Bean 的注册. ⼀般与 @Configuration 和 @Bean 配合使⽤。简单说,Spring 在解析@Configuration 配置类的时候,如果该配置类增加了 @Conditional 注解,那么会根据该注解配置的条件来决定是否要实现 Bean 的装配.

@Conditonal 注解类声明代码如下, 该注解可以接收⼀个Condition 的数组, 位于org.springframework.context.annotation 包下.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}

Condition 是⼀个函数式接⼝, 提供了 matches ⽅法, 它主要是提供⼀个条件匹配规则, 返回 true 表⽰可以注⼊ Bean, 反之不注⼊.

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
条件装配案例

基于 @Conditional 实现⼀个条件装配案例

⾃定义⼀个 Condition , 如果当前JDK版本是21, 就注册JDK21, 如果当前JDK版本是17, 就注册JDK17。

@SpringBootTest
public class ConditionalTest {
//    @Autowired
//    private JDK17 jdk17;
    @Test
    void test(){
        System.out.println("执行test的方法");
    }
}


@Configuration
class AppConfig{
    //如果Jdk17Conditional matches返回true,则注册该Bean
    @Bean
    @Conditional(Jdk17Conditional.class)
    public JDK17 jdk17(){
        System.out.println("JDK17初始化");
        return new JDK17();
    }

    //如果Jdk21Conditional matches返回true,则注册该Bean
    @Bean
    @Conditional(Jdk21Conditional.class)
    public JDK21 jdk21(){
        System.out.println("JDK21初始化");
        return new JDK21();
    }
}

class JDK17{}

class JDK21{}

class Jdk17Conditional implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return JavaVersion.getJavaVersion().equals(JavaVersion.SEVENTEEN);
    }
}

class Jdk21Conditional implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return JavaVersion.getJavaVersion().equals(JavaVersion.TWENTY_ONE);
    }
}

可以观察, 如果JDK版本为17时, jdk17就会被注册. 如果JDK为21, JDK21就会被注册.

扩展注解

@Conditional 注解是 Spring 框架中⼀个强⼤的⼯具, 它允许根据特定条件来控制 Bean 的创建和注册. Spring Boot 在此基础上提供了⼀系列的扩展注解,使得条件注册 Bean 更加⽅便和直观. 以下是⼀些常⽤的 @Conditional 扩展注解及其使⽤场景:

• @ConditionalOnBean

当容器中存在指定的 Bean 时, 使⽤这个注解的配置类或 Bean ⽅法才会⽣效

只有当 DependencyBean 存在时, MyBean 才会被注册

@ConditionalOnBean(DependencyBean.class)
public class MyBean {
// ...
}

• @ConditionalOnMissingBean

与 @ConditionalOnBean 相反,只有当容器中不存在指定的 Bean 时,才会注册使⽤这个注解的 Bean

如果 DependencyBean 不存在, MyBean 会被注册。

@ConditionalOnMissingBean(DependencyBean.class)
public class MyBean {
// ...

• @ConditionalOnClass

当类路径上存在指定的类时,使⽤这个注解的配置类或 Bean ⽅法才会⽣效.

如果 DependencyClass 存在于类路径上, MyBean 会被注册

@ConditionalOnMissingClass

当类路径上不存在指定的类时,使⽤这个注解的配置类或 Bean ⽅法才会⽣效.

如果 DependencyClass 不存在于类路径上, MyBean 会被注册

• @ConditionalOnProperty:

这个注解允许基于配置⽂件中的属性值来注册 Bean. 你可以指定属性的名称和期望的值, 如果配置的属性存在且匹配指定的值, 则相应的 Bean 会被注册

如果 my.property 的值是 true ,则 MyBean 会被注册

@ConditionalOnProperty(name = "my.property", havingValue = "true")
public class MyBean {
// ...
}

这些注解只需要添加到 @Configuration 配置类的类级别或者⽅法级别,然后根据每个注解的作⽤传参就⾏.

@Conditional 注解及扩展注解在实际项⽬中有⼴泛的应⽤场景, 尤其是在需要根据特定条件动态配置 Bean 的情况下, 在第三⽅的依赖中也经常出现. ⽐如Redis的⾃动配置⽂件:

⼯具封装:

基于以上知识的了解, 我们来完成公共模块的开发

⾃动配置, 加载 RedisConfig , 根据条件是否加载 Redis对象 具体步骤如下:

1. 在resources下创建 METAINF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports⽂件内容设置需要Spring⾃动加载的对象

com.jqq.common.config.RedisConfig

2. 设置条件装配

如果配置⽂件中配置了redis相关的内容, 我们进⾏Redis相关内容的加载

@Configuration
public class RedisConfig {
    @Bean
    @ConditionalOnProperty(prefix = "spring.data.redis",name = "host")
    public Redis redis(StringRedisTemplate redisTemplate){
        return new Redis(redisTemplate);
    }
}
项⽬应⽤:

⽤⼾注册后, 数据存储到数据库中后, 再存储到Redis⼀份, 获取⽤⼾信息时, 从Redis中获取. Redis格式定义:

key设置为: user:zhangsan

为了避免键名冲突, 通常会为key添加命名空间前缀, 这个前缀通常反映了键的⽤途或归属, ⽐如 user: 、 session: 、 product: 

value设置为⽤⼾信息的Json字符串

JSON⼯具类

在 Java 中,有⼏个流⾏的 JSON 处理库,常⻅的有: Jackson, Gson 和 Fastjson

1. Jackson

Jackson 是⼀个开源的 JSON 处理库, ⼴泛⽤于将 Java 对象与 JSON 数据进⾏互相转换, Jackson 是 Spring 框架的默认JSON 解析器, 拥有活跃的社区⽀持和频繁的更新迭代.

ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(yourObject); 
YourClass obj = mapper.readValue(jsonString, YourClass.class); 

2. Gson

Gson 是 Google 开发的⼀个 JSON 库.

Gson gson = new Gson();

String json = gson.toJson(yourObject);

YourClass obj = gson.fromJson(json, YourClass.class); 

3. Fastjson

https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2

Fastjson 是阿⾥巴巴开源的 JSON 处理库, 具有⾼性能的特点.该项⽬中我们采⽤fastjson来进⾏数据的处理.

使⽤如下: 添加依赖:

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.52</version>
</dependency>

封装⼯具类:

@Slf4j
public class JsonUtil<T> {
    /**
     * 字符串转对象
     */
    public static <T> T parseJson(String json ,Class<T> clazz){
        if(!StringUtils.hasLength(json) || clazz == null){
            return null;
        }
        try{
            return JSON.parseObject(json,clazz);
        }catch (Exception e){
            log.error("JsonUtil parseJson error,e:{} ",e);
            return null;
        }
    }


    /**
     * 对象转字符串
     */
    public static String toJson(Object o){
        try{
            return o == null ? null : JSON.toJSONString(o);
        }catch (Exception e){
            log.error("JsonUtil toJson error,e:{} ",e);
            //看需求是否需要抛出异常
            return null;
        }
    }
}
⽤⼾注册修改:

UserServiceImpl修改如下:

注⼊Redis⼯具类

@Autowired
    private Redis redis;

    //redis超时时间:14天
    private static final long EXPIRE_TIME = 14*24*60*60;
    private static final String USER_PREFIX = "user";

⽤⼾注册添加Redis存储逻辑

@Override
    public Integer register(UserInfoRegisterRequest registerRequest) {
        //校验
        checkUserInfo(registerRequest);
        //用户注册,插入数据库
        UserInfo userInfo = BeanConvert.convertUserInfoEncrypt(registerRequest);
        try{
            int result = userInfoMapper.insert(userInfo);
            if(result == 1){
                //存储数据到redis中
                //存储失败,就从数据库查询,此处异常不处理
                redis.set(buildKey(userInfo.getUserName()), JsonUtil.toJson(userInfo),EXPIRE_TIME);
                return userInfo.getId();
            }else{
                throw new BlogException("用户注册失败");
            }
        }catch (Exception e){
            log.error("用户注册失败,e:{}",e);
            throw new BlogException("用户注册失败");
        }
    }

    private String buildKey(String userName) {
        return redis.buildKey(USER_PREFIX,userName);
    }
⽤⼾登录修改;

⽤⼾注册时, 存储数据到Redis中, 那么⽤⼾登录时, 就可以先从Redis中获取数据, 如果Redis中数据不存在(Redis数据可能过期), 则从数据库中查找.

private UserInfo QueryUserInfo(String userName) {
        //先从redis中获取
        String key = buildKey(userName);
        Boolean aBoolean = redis.hasKey(key);
        if(aBoolean){
            //从redis中读取
            log.info("从redis中get数据,key:{}",key);
            String userJson = redis.get(key);
            UserInfo userInfo = JsonUtil.parseJson(userJson,UserInfo.class);
            return userInfo == null ? selectUserInfoByName(userName):userInfo;
        }else {
            log.info("从mysql中get数据,userName:{}",userName);
            UserInfo userInfo = selectUserInfoByName(userName);
            redis.set(key, JsonUtil.toJson(userInfo),EXPIRE_TIME);
            return userInfo;
        }
    }

测试:

⽤⼾注册: http://127.0.0.1:10030/user/register 程序执⾏成功观察Redis

⽤⼾登录 http://127.0.0.1:10030/user/login 程序执⾏成功,观察⽇志, 从Redis中获取值

把Redis中的值删除, 再次观察⽇志, 从mysql中取值成功

引⼊RabbitMQ:

⽤⼾注册完成之后, 系统会给⽤⼾发送⼀封邮件通知. 

但是邮件发送成功失败并不影响我们⽤⼾注册成功.我们可以采⽤RabbitMQ来进⾏服务的解耦.

RabbitMQ介绍:

RabbitMQ 是⼀个开源的消息代理和队列服务器,⼴泛⽤于实现消息队列和事件通知. 它⽀持多种消息协议,并且具有⾼可⽤性、灵活的路由、可靠的消息传递和易于使⽤的管理界⾯.

RabbitMQ简单介绍参考:
1. 什么是 MQ?

MQ( Message queue ), 从字⾯意思上看, 本质是个队列, FIFO 先⼊先出,只不过队列中存放的内容是消息(message) ⽽已. 消息可以⾮常简单,⽐如只包含⽂本字符串, JSON等,也可以很复杂, ⽐如内嵌对象.

MQ多⽤于分布式系统之间进⾏通信. 系统之间的调⽤通常有两种⽅式:

同步通信:

直接调⽤对⽅的服务, 数据从⼀端发出后⽴即就可以达到另⼀端.

异步通信:

数据从⼀端发出后,先进⼊⼀个容器进⾏临时存储,当达到某种条件后,再由这个容器发送给另⼀端. 容器的⼀个具体实现就是MQ( message queue )

RabbitMQ 就是MQ的⼀种实现

2. MQ的作⽤

MQ主要⼯作是接收并转发消息, 在不同的应⽤场景下可以展现不同的作⽤

1. 异步解耦: 在业务流程中, ⼀些操作可能⾮常耗时, 但并不需要即时返回结果. 可以借助MQ把这些操作异步化, ⽐如 ⽤⼾注册后发送注册短信或邮件通知, 可以作为异步任务处理, ⽽不必等待这些操作完成后才告知⽤⼾注册成功.

2. 流量削峰: 在访问量剧增的情况下, 应⽤仍然需要继续发挥作⽤, 但是是这样的突发流量并不常⻅. 如果以能处理这类峰值为标准⽽投⼊资源,⽆疑是巨⼤的浪费. 使⽤MQ能够使关键组件⽀撑突发访问压⼒, 不会因为突发流量⽽崩溃. ⽐如秒杀或者促销活动, 可以使⽤MQ来控制流量, 将请求排队, 然后系统根据⾃⼰的处理能⼒逐步处理这些请求.

3. 异步通信: 在很多时候应⽤不需要⽴即处理消息, MQ提供了异步处理机制, 允许应⽤把⼀些消息放⼊MQ中, 但并不⽴即处理它,在需要的时候再慢慢处理.

4. 消息分发: 当多个系统需要对同⼀数据做出响应时, 可以使⽤MQ进⾏消息分发. ⽐如⽀付成功后, ⽀付系统可以向MQ发送消息, 其他系统订阅该消息, ⽽⽆需轮询数据库.

5. 延迟通知: 在需要在特定时间后发送通知的场景中, 可以使⽤MQ的延迟消息功能, ⽐如在电⼦商务平台中,如果⽤⼾下单后⼀定时间内未⽀付,可以使⽤延迟队列在超时后⾃动取消订单

6. ......

3. 为什么选择 RabbitMQ

⽬前业界有很多的MQ产品, 例如RabbitMQ, RocketMQ, ActiveMQ, Kafka, ZeroMQ等, 也有直接使⽤Redis充当消息队列的案例, 这些消息队列, 各有侧重, 也没有好坏, 只有适合不适合, 在实际选型时, 需要结合⾃⾝需求以及MQ产品特征, 综合考虑

以下介绍⼀下当前最主流的3种MQ产品

Kafka:

Kafka⼀开始的⽬的就是⽤于⽇志收集和传输,追求⾼吞吐量, 性能卓越, 单机吞吐达到⼗万级, 在⽇志领域⽐较成熟, 功能较为简单,主要⽀持简单的 MQ 功能, 如果有⽇志采集需求,肯定是⾸选kafka了。

RocketMQ:

RocketMQ采⽤Java语⾔开发, 由阿⾥巴巴开源, 后捐赠给了Apache.

它在设计时借鉴了Kafka,并做出了⼀些⾃⼰的改进, ⻘出于蓝⽽胜于蓝, 经过多年双⼗⼀的洗礼, 在可⽤性、可靠性以及稳定性等⽅⾯都有出⾊的表现. 适合对于可靠性⽐较⾼,且并发⽐较⼤的场景, ⽐如互联⽹⾦融. 但⽀持的客⼾端语⾔不多, 且社区活跃度⼀般

RabbitMQ:

采⽤Erlang语⾔开发, MQ 功能⽐较完备, 且⼏乎⽀持所有主流语⾔开源提供的界⾯也⾮常友好, 性能较好, 吞吐量能达到万级, 社区活跃度也⽐较⾼,⽐较适合中⼩型公司, 数据量没那么⼤, 且并发没那么⾼的场景.

综合: 由于 RabbitMQ 的综合能⼒较强, 咱们这边的项⽬没有那么⼤的⾼并发, 且RabbitMQ社区⽐较成熟, 管理界⾯友好, 所以咱们接下来主要RabbitMQ的使⽤

4. RabbitMQ 的安装

Ubuntu 环境安装

RabbitMQ已经包含在标准的Ubuntu仓库中, 然⽽,包含的版本通常⽐最新的RabbitMQ发⾏版落后很多,可能提供的RabbitMQ版本已经不⽀持. RabbitMQ团队制作了⾃⼰的软件包,并使⽤Cloudsmith进⾏分发

具体操作可以参考: Installing on Debian and Ubuntu | RabbitMQ

由于该种⽅法安装⽐较复杂, 使⽤Ubuntu仓库中的版本来安装

⼀. 安装Erlang

RabbitMq需要Erlang语⾔的⽀持,在安装rabbitMq之前需要安装erlang
 

更新软件包

sudo apt-get update

安装erlang

sudo apt-get install erlang

查看erlang版本

erl

退出命令:

halt().

⼆. 安装RabbitMQ
 

更新软件包

sudo apt-get update

安装RabbitMQ

sudo apt-get install rabbitmq-server

确认安装结果

systemctl status rabbitmq-server

三. 安装RabbitMQ管理界⾯

默认是不安装管理界⾯的

 rabbitmq-plugins enable rabbitmq_management

四. 启动服务并访问

1. 启动服务

若服务已经启动了, 此步省略

sudo service rabbitmq-server start 

systemctl status rabbitmq-server

2. 通过 IP:port 访问界⾯

http://110.41.51.65:15672/ (15672 为默认端⼝号, 云服务器需要开启端⼝))

服务端通信默认端口号5672

默认⽤⼾名和密码都是: guest

rabbitmq从3.3.0开始禁⽌使⽤guest/guest权限通过除localhost外的访问, 

3. 添加管理员⽤⼾

a) 添加⽤⼾admin, 密码:admin

#rabbitmqctl add_user ${账号} ${密码}

rabbitmqctl add_user admin admin 

b) 给⽤⼾添加权限

#rabbitmqctl set_user_tags ${账号} ${⻆⾊名称}

rabbitmqctl set_user_tags admin administrator 

以下⻆⾊可选:

RabbitMQ⽤⼾⻆⾊分为Administrator、Monitoring、Policymaker、Management、Impersonator、None共六种⻆⾊

1. Administrator 超级管理员,可登陆管理控制台(启⽤management plugin的情况下),可查看所有的信息,并且可以对⽤⼾,策略(policy)进⾏操作

2. Monitoring 监控者,可登陆管理控制台(启⽤management plugin的情况下),同时可以查看rabbitmq节点的相关信息(进程数,内存使⽤情况,磁盘使⽤情况等)。

3. Policymaker 策略制定者,可登陆管理控制台(启⽤management plugin的情况下),同时可以对policy进⾏管理。但⽆法查看节点的相关信息.

4. Management 普通管理者,仅可登陆管理控制台(启⽤management plugin的情况下),⽆法看到节点信息,也⽆法对策略进⾏管理.

5. Impersonator 模拟者,⽆法登录管理控制台。

6. None 其他⽤⼾,⽆法登陆管理控制台,通常就是普通的⽣产者和消费者。

c) 通过IP:port访问, 并使⽤刚才设置的⽤⼾名和密码登录 http://110.41.51.65:15672

5. RabbitMQ 核⼼概念

在安装完RabbitMQ之后,  并安装了管理界⾯

界⾯上的导航栏共分6部分, 这6部分分别是什么意思呢, 我们先看看RabbitMQ的⼯作流程

RabbitMQ是⼀个消息中间件, 也是⼀个⽣产者消费者模型. 它负责接收, 存储并转发消息.

消息传递的过程类似邮局.

当你要发送⼀个邮件时,你把你的邮件放到邮局,邮局接收邮件, 并通过邮递员送到收件⼈的⼿上.

按照这个逻辑, Producer 就类似邮件发件⼈. Consumer 就是收件⼈, RabbitMQ就类似于邮局

Producer和Consumer:

Producer: ⽣产者, 是RabbitMQ Server的客⼾端, 向RabbitMQ发送消息

Consumer: 消费者, 也是RabbitMQ Server的客⼾端, 从RabbitMQ接收消息

Broker:其实就是RabbitMQ Server, 主要是接收和收发消息

• ⽣产者(Producer)创建消息, 然后发布到RabbitMQ中. 在实际应⽤中, 消息通常是⼀个带有⼀定业务逻辑结构的数据, ⽐如JSON字符串. 消息可以带有⼀定的标签, RabbitMQ会根据标签进⾏路由, 把消息发送给感兴趣的消费者(Consumer).

• 消费者连接到RabbitMQ服务器, 就可以消费消息了, 消费的过程中, 标签会被丢掉. 消费者只会收到消息, 并不知道消息的⽣产者是谁, 当然消费者也不需要知道.

• 对于RabbitMQ来说,⼀个RabbitMQ Broker可以简单地看作⼀个RabbitMQ服务节点, 或者RabbitMQ服务实例. ⼤多数情况下也可以将⼀个RabbitMQ Broker看作⼀台RabbitMQ服务器

Connection和Channel:

Connection: 连接. 是客⼾端和RabbitMQ服务器之间的⼀个TCP连接. 这个连接是建⽴消息传递的基础, 它负责传输客⼾端和服务器之间的所有数据和控制信息.(端口号默认:5672)

Channel: 通道, 信道. Channel是在Connection之上的⼀个抽象层. 在 RabbitMQ 中, ⼀个TCP连接可以有多个Channel, 每个Channel 都是独⽴的虚拟连接. 消息的发送和接收都是基于 Channel的.

通道的主要作⽤是将消息的读写操作复⽤到同⼀个TCP连接上,这样可以减少建⽴和关闭连接的开销, 提⾼性能.

Virtual host:

Virtual host: 虚拟主机. 这是⼀个虚拟概念. 它为消息队列提供了⼀种逻辑上的隔离机制. 对于RabbitMQ⽽⾔, ⼀个 BrokerServer 上可以存在多个 Virtual Host. 当多个不同的⽤⼾使⽤同⼀个 RabbitMQ Server 提供的服务时,可以虚拟划分出多个 vhost,每个⽤⼾在⾃⼰的 vhost 创建 exchange/queue 等

类似MySQL的"database", 是⼀个逻辑上的集合. ⼀个MySQL服务器可以有多个database.

Queue

Queue: 队列, 是RabbitMQ的内部对象, ⽤于存储消息.

多个消费者, 可以订阅同⼀个队列

Exchange:

Exchange: 交换机. message 到达 broker 的第⼀站, 它负责接收⽣产者发送的消息, 并根据特定的规则把这些消息路由到⼀个或多个Queue列中.

Exchange起到了消息路由的作⽤,它根据类型和规则来确定如何转发接收到的消息.

类似于发快递之后, 物流公司怎么处理呢, 根据咱们的地址来分派这个快递到不同的站点, 然后再送到收件⼈⼿⾥. 这个分配的⼯作,就是交换机来做的

RabbitMQ⼯作流程:

理解了上⾯的概念之后, 再来回顾⼀下这个图, 来看RabbitMQ的⼯作流程

1. Producer ⽣产了⼀条消息

2. Producer 连接到RabbitMQBroker, 建⽴⼀个连接(Connection),开启⼀个信道(Channel)

3. Producer 声明⼀个交换机(Exchange), 路由消息

4. Producer 声明⼀个队列(Queue), 存放信息

5. Producer 发送消息⾄RabbitMQ Broker

6. RabbitMQ Broker 接收消息, 并存⼊相应的队列(Queue)中, 如果未找到相应的队列, 则根据⽣产者的配置, 选择丢弃或者退回给⽣产者.

6. AMQP

AMQP(Advanced Message Queuing Protocol)是⼀种⾼级消息队列协议, AMQP定义了⼀套确定的消息交换功能, 包括交换器(Exchange), 队列(Queue) 等. 这些组件共同⼯作, 使得⽣产者能够将消息发送到交换器. 然后由队列接收并等待消费者接收. AMQP还定义了⼀个⽹络协议, 允许客⼾端应⽤通过该协议与消息代理和AMQP模型进⾏交互通信

RabbitMQ是遵从AMQP协议的,换句话说,RabbitMQ就是AMQP协议的Erlang的实现(当然RabbitMQ还⽀持STOMP2, MQTT2等协议). AMQP的模型结构和RabbitMQ的模型结构是⼀样的.

7. web界⾯操作

RabbitMQ管理界⾯上的Connections,Channels, Exchange, Queues 就是和上⾯流程图的概念是⼀样的, Overview就是视图的意思, Admin是⽤⼾管理.

我们在操作RabbitMQ前, 需要先创建Virtual host 接下来看具体操作:

⽤⼾相关操作:

添加⽤⼾

a) 点击 Admin -> Add user

b) 设置账号密码及权限

①: 设置账号

②: 设置密码

③: 确认密码

④: 设置权限

添加完成后, 点击[Add user]

c) 观察⽤⼾是否添加成功

⽤⼾相关操作:

a) 点击要删除的⽤⼾, 查看⽤⼾详情

b) 在⽤⼾详情⻚⾯, 进⾏更新或删除操作

• 设置对虚拟机的操作权限

• 更新/删除⽤⼾

退出当前⽤⼾

虚拟主机相关操作:

创建虚拟主机

在Admin标签⻚下, 点击右侧 Virtual Hosts -> Add a new virtual host 设置虚拟主机名称

观察设置结果

此操作会为当前登录⽤⼾设置虚拟主机

8. SpringBoot 继承RabbitMQ

对于RabbitMQ开发, Spring 也提供了⼀些便利. Spring 和RabbitMQ的官⽅⽂档对此均有介绍 Spring官⽅: Spring AMQP

RabbitMQ 官⽅:RabbitMQ tutorial - "Hello World!" | RabbitMQ

RabbitMQ 共提供了7种⼯作模式, 进⾏消息传递, :

https://www.rabbitmq.com/tutorials

下⾯来看如何基于SpringBoot 进⾏RabbitMQ的开发. 步骤如下:

1. 引⼊依赖

2. 编写yml配置,基本信息配置

3. 编写⽣产者代码

4. 编写消费者代码

        a. 定义监听类, 使⽤@RabbitListener注解完成队列监听

5. 运⾏观察结果

引⼊依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

也可以通过创建项⽬时, 加⼊依赖

添加配置

#配置RabbitMQ的基本信息

spring: 
    rabbitmq:
        host: 110.41.51.65 
        port: 15673    默认为5672
        username: study 
        password: study
        virtual-host: jqq    默认值为 / 

或以下配置

amqp://username:password@Ip:port/virtual-host

spring: 
    rabbitmq:
        addresses: amqp://study:study@47.108.157.13:5672/jqq

编写⽣产者代码

声明队列

@Configuration
public class RabbitConfig {
    @Bean("helloQueue")
    public Queue queue(){
        return QueueBuilder.durable("hello").build();//durable表示持久化 ,队列名称:hello
    }
}

测试发送消息

@SpringBootTest
public class RabbitMQTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void send(){
        rabbitTemplate.convertAndSend("","hello","hello rabbitMQ");
    }
}

运⾏程序, 观察消息发送成功

编写消费者代码

定义监听类

@Component
public class HelloQueueListener {
    @RabbitListener(queues = "hello")
    public void handler(Message message){
        System.out.println("收到消息:"+message);
    }
}

@RabbitListener 是Spring框架中⽤于监听RabbitMQ队列的注解, 通过使⽤这个注解,可以定义⼀个⽅法, 以便从RabbitMQ队列中接收消息. 该注解⽀持多种参数类型,这些参数类型代表了从RabbitMQ接收到的消息和相关信息. 以下是⼀些常⽤的参数类型:

1. String :返回消息的内容

2. Message ( org.springframework.amqp.core.Message ): Spring AMQP的Message 类,返回原始的消息体以及消息的属性, 如消息ID, 内容, 队列信息等.

3. Channel ( com.rabbitmq.client.Channel ):RabbitMQ的通道对象, 可以⽤于进⾏更⾼级的操作,如⼿动确认消息.

运⾏程序, 观察结果

消费者打印消息内容

集成RabbitMQ:

引⼊依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

添加配置, 并配置⼿动确认⽅式

 #配置RabbitMQ的基本信息

spring: 
    rabbitmq:
        host: 110.41.51.65 
        port: 15673    默认为5672
        username: study 
        password: study
        virtual-host: jqq    默认值为 / 

或以下配置

amqp://username:password@Ip:port/virtual-host

spring: 
    rabbitmq:
        addresses: amqp://study:study@47.108.157.13:5672/jqq
    listener:
        simple:
            acknowledge-mode: manual   配置⼿动确认

声明队列, 交换机

    @Bean("userQueue")
    public Queue userQueue(){
        return QueueBuilder.durable(Constants.USER_QUEUE_NAME).build();
    }

    @Bean("userExchange")
    public FanoutExchange userExchange(){
        return ExchangeBuilder.fanoutExchange(Constants.USER_EXCHANGE_NAME).durable(true).build();
    }

    @Bean("userBinding")
    public Binding userBinding(@Qualifier("userQueue") Queue userQueue,@Qualifier("userExchange") FanoutExchange userExchange){
        return BindingBuilder.bind(userQueue).to(userExchange);
    }

发送消息

//发送消息
userInfo.setPassword("");             

rabbitTemplate.convertAndSend(Constants.USER_EXCHANGE_NAME,"",JsonUtil.toJson(userInfo));
                

消费消息

@Slf4j
@Component
public class UserQueueListener {
    @RabbitListener(queues = Constants.USER_QUEUE_NAME)
    public void handler(Message message,Channel channel) throws IOException {
        //发送邮件
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            String body = new String(message.getBody());
            log.info("收到用户信息,body:{}",body);
            //发送邮件
            //确认
            channel.basicAck(deliveryTag,true);
        }catch (Exception e){
            //否定确认
            channel.basicNack(deliveryTag,true,true);
            log.error("邮件发送失败,e:",e);
        }
    }
}

也可以使⽤ @RabbitListener 来声明队列, 交换机

@RabbitListener是⼀个功能强⼤的注解。这个注解⾥⾯可以注解配置@QueueBinding、@Queue、@Exchange直接通过这个组合注解⼀次性搞定多个交换机、绑定、路由、并且配置监听功能等

@RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = Constants.USER_QUEUE_NAME,durable = "true"),
            exchange = @Exchange(value = Constants.USER_EXCHANGE_NAME,durable = "true",type = ExchangeTypes.FANOUT)
    ))
    public void handler(Message message,Channel channel) throws IOException {
        //发送邮件
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            String body = new String(message.getBody());
            log.info("收到用户信息,body:{}",body);
            //发送邮件
            //确认
            channel.basicAck(deliveryTag,true);
        }catch (Exception e){
            //否定确认
            channel.basicNack(deliveryTag,true,true);
            log.error("邮件发送失败,e:",e);
        }
    }

测试: http://127.0.0.1:10030/user/register

⽤⼾注册成功时, 从⽇志观察, 消费者接收到消息

引⼊邮件发送:

基于 JavaMail API,Spring Boot 提供了⼀个⽤于发送邮件的Starter: spring-boot-startermail . 通过简单的配置即可实现邮件发送功能. 参考: Spring Boot 发送邮件

快速上⼿

引⼊依赖

<!--邮件发送依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

配置邮箱

spring:
    mail:
        host: smtp.qq.com       #需要在设置中开启 smtp
        username: 3471714098@qq.com  #发件⼈的邮箱
        password: sndvxlvhcfcidbbe      #邮箱的授权码, 并⾮个⼈密码
        port: 465  # SSL加密端口(必须用465,不能用25)
        protocol: smtp
        default-encoding: UTF-8
        properties:
          personal: "JQQ"
          mail:
            smtp:
              ssl:
                  enable: true  # 强制启用SSL加密(关键配置)
              socketFactory:
                  class: javax.net.ssl.SSLSocketFactory  # SSL连接工厂
              auth: true  # 启用认证
host: smtp.qq.com#需要在设置中开启 smtp
username: XXXXX@qq.com

#发件⼈的邮箱

password: XXXXXXXX         

password:#邮箱的授权码, 并⾮个⼈密码

邮箱授权码通常在: 邮箱设置-》第三⽅服务-》开启IMAP/SMTP服务-》获取授权码

测试发送邮件

@SpringBootTest
public class MailTest {

    @Autowired
    private JavaMailSender mailSender;

    @Test
    void send() throws Exception {
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, false);
        helper.setFrom("3471714098@qq.com","JQQ");
        helper.setTo("2147406100@qq.com");
        helper.setSubject("测试邮件发送");
        helper.setText("<h1>注册用户成功</h1>",true);

        mailSender.send(mimeMessage);

    }
}

⼯具封装:

把邮件发送功能封装成⼀个⼯具

发件⼈邮件和名称从配置⽂件中读取, 关于邮箱的配置会被封装到 MailProperties 这个对象中.

public class Mail {
    private JavaMailSender mailSender;

    private MailProperties mailProperties;

    public Mail(JavaMailSender mailSender,MailProperties mailProperties) {
        this.mailSender = mailSender;
        this.mailProperties = mailProperties;
    }

    public void send(String to,String subject,String content) throws Exception {
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, false);
        String personal = Optional.ofNullable(mailProperties.getProperties().get("personal")).orElse(mailProperties.getUsername());
        helper.setFrom(mailProperties.getUsername(),personal);//发件人
        helper.setTo(to); //收件人
        helper.setSubject(subject); //邮件主题
        helper.setText(content,true);
        mailSender.send(mimeMessage);

    }
}
@Configuration
public class MailConfig {
    @Bean
    @ConditionalOnProperty(prefix = "spring.mail",name = "username")
    public Mail mail(JavaMailSender javaMailSender, MailProperties mailProperties){
        return new Mail(javaMailSender,mailProperties);
    }
}

把 com.bite.common.config.MailConfig 添加到SpringBoot ⾃动配置⽂件中org.springframework.boot.autoconfigure.AutoConfiguration.imports

项⽬应⽤:

配置yml

spring:
    mail:
        host: smtp.qq.com       #需要在设置中开启 smtp
        username: 3471714098@qq.com  #发件⼈的邮箱
        password: sndvxlvhcfcidbbe      #邮箱的授权码, 并⾮个⼈密码
        port: 465  # SSL加密端口(必须用465,不能用25)
        protocol: smtp
        default-encoding: UTF-8
        properties:
          personal: "JQQ"
          mail:
            smtp:
              ssl:
                  enable: true  # 强制启用SSL加密(关键配置)
              socketFactory:
                  class: javax.net.ssl.SSLSocketFactory  # SSL连接工厂
              auth: true  # 启用认证

当消费者收到MQ消息后, 发送消息

@Autowired
    private Mail mail;

mail.send(userInfo.getEmail(), "恭喜加入J家军",buildContent(userInfo.getUserName()));

public String buildContent(String userName){
        StringBuilder builder = new StringBuilder();
        builder.append("尊敬的").append(userName).append(",您好!").append("<br/>");
        builder.append("感谢您注册成为我们J家军的一员!我们很高兴您加入我们的大家庭!<br/>");
        builder.append("您的注册信息如下:用户名:").append(userName).append("<br/>");
        builder.append("为了您的账户安全,请妥善保管您的登录信息...");
        return builder.toString();
    }

7. 前端开发&部署

Nignx简单介绍参考

1. Nginx 介绍

Nginx是⼀款开源的 Web 服务器软件, 可以⽤作反向代理、负载均衡器. 它因⾼性能, ⾼稳定性, 丰富的功能和低资源消耗⽽闻名.

Nginx本⾝也是⼀个静态资源的服务器, 当只有静态资源的时候, 可以使⽤Nginx来做服务器实现部署.

2. Nignx 下载及安装

官⽹下载: https://nginx.org/en/download.html

安装

Windows:

1. 下载完成后,将压缩包解压到本地, 放在希望安装的地⽅即可

2. 启动nginx

双击nginx.exe, 启动Nginx, 访问: http://127.0.0.1:80/

出现以下界⾯, 说明nginx启动成功

3. 启停命令

我们也可以掌握⼀些命令来启停nginx. 进⼊Nginx的安装⽬录cmd
 

启动

start nginx

停⽌

.\nginx.exe -s stop

或者.\nginx.exe -s quit

stop是快速停⽌nginx,可能并不保存相关信息;quit是完整有序的停⽌nginx,并保存相关信息

Ubuntu

安装命令
 

更新软件包

sudo apt-get update

安装nginx

sudo apt-get install nginx

查看nginx版本

nginx -v

启动nginx

systemctl start nginx

查看nginx状态

systemctl status nginx

开放80端⼝号, 访问Nginx

3. 配置⾃⼰项⽬

Windows

修改 conf/nginx.conf

Nginx 默认端⼝号为80, 访问路径为html/index.html 或者index.html, 可以改成⾃⼰的⽬录

访问 http://127.0.0.1:80, 就可以看到⾃⼰配置的项⽬了.

Ubuntu

修改配置⽂件 /etc/nginx/sites-enabled/default 修改访问路径为⾃⼰的⽬录和⽂件

本地部署前端服务:

部署前端:

把Spring-boot中开发好的前端代码, 放在某个⽬录下, 配置Nginx访问路径

修改访问路径

因为前后端完全分离, 不在⼀个服务中, 所以前端访问时, 需要加上完整url

const baseURL = "http://127.0.0.1:10030";

$(document).ajaxSend(function (e, xhr, opt) {
    opt.url = baseURL+opt.url;
    var user_token = localStorage.getItem("user_token");
    xhr.setRequestHeader("user_token", user_token);
});

后端统⼀⼊⼝为从⽹关服务访问, 所以前端请求需要加上 http://127.0.0.1:10030 修改common.js, 给所以请求url添加前缀

后端允许跨域:

跨域(Cross-Origin)指的是浏览器阻⽌前端⽹⻚从⼀个域名(Origin)向另⼀个域名的服务器发送请求. 具体来说,⼀个⻚⾯的协议, 域名, 端⼝三者任意⼀个与请求的⽬标地址不同, 就被视为跨域请求.

当前前端和后端不在⼀个服务, 端⼝号也就不同, 就会存在跨域问题

配置⽹关项⽬允许跨域: 参考:CORS Configuration

spring:
    gateway:
      globalcors:
        # 解决options请求被拦截的问题
        add-to-simple-url-handler-mapping: true
        cors-configurations:
          '[/**]':
            allowedOriginPatterns: "*" #设置允许跨域的来源
            allowedMethods: "*"
            allowedHeaders: "*"
            allowCredentials: true

测试博客系统的各个功能是否正常.

⽤⼾注册,⻚⾯准备

后端增加了⽤⼾注册功能, 前端也需要添加相应的⻚⾯ 

⽤⼾登录⻚⾯添加

            <div class="row">
                <span>还没账号? 点此<a href="blog_register.html">注册</a></span>
            </div>

前后端交互

后端接⼝定义

[请求]

/user/register

[参数]

contentType: application/json

{

"userName":"wangwu",

"password":"456789",

"githubUrl": "https://gitee.com/bubble-fish666/spring-cloud",

"email": "999@126.com" }

[响应] {

"code": 200,

"errMsg": null,

"data": 3 //⽤⼾ID

}

前端发送请求:

        function register() {
            $.ajax({
                type:"post",
                url:"/user/register",
                contentType:"application/json",
                data: JSON.stringify({
                    "userName": $("#username").val(),
                    "password": $("#password").val(),
                    "email": $("#email").val(),
                    "githubUrl": $("#githubUrl").val()
                }),
                success:function(result){
                    if(result.code == 200&& result.data!=null){
                        //注册成功
                        location.href=("blog_register_success.html");
                    }else{
                        alert("注册失败");
                    }
                }
            });
        }

8. Linux部署

环境准备

• Mysql

• Redis

• RabbitMQ

• Nacos

• Nginx

部署前端

修改访问url:

const baseURL = "http://47.108.157.13:10030";

修改Nginx 访问路径

多环境配置

SpringBoot 还可以通过 --- 来区分环境配置

博客服务, 设置mysql, nacos

server:
  port: 8080
spring:
  application:
    name: blog-service
  cloud:
    loadbalancer:
      nacos:
        enabled: true
    nacos:
      discovery:
        server-addr: ${nacos.address}
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring_cloud_blog?characterEncoding=utf8&useSSL=false
    username: root
    password: ${mysql.password}
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
  file:
    name: blog.log

---
#开发环境
spring:
  config:
    activate:
      on-profile: dev
nacos:
  address: 152.136.172.144:10020
mysql:
  password: "123456"

---
#生产环境
spring:
  config:
    activate:
      on-profile: prod
nacos:
  address: 127.0.0.1:10020
mysql:
  password: Jqq200506,

⽤⼾服务:

server:
  port: 9090
spring:
  application:
    name: user-service
  cloud:
    loadbalancer:
      nacos:
        enabled: true
    nacos:
      discovery:
        server-addr: ${nacos.address}
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring_cloud_user?characterEncoding=utf8&useSSL=false
    username: root
    password: ${mysql.password}
    driver-class-name: com.mysql.cj.jdbc.Driver
  #redis配置
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      timeout: 60s #连接空闲超过N(s秒、ms毫秒)后关闭,0为禁⽤,这⾥配置值和tcpkeepalive值⼀致
      lettuce:
        pool:
          max-active: 8 #允许最⼤连接数
          max-idle: 8 #最⼤空闲连接数, 默认8
          min-idle: 0 #最⼩空闲连接数
          max-wait: 5s #请求获取连接等待时间
  rabbitmq:
    username: jqq
    password: Jqq200506,
    virtual-host: blog
    addresses: ${rabbitmq.address}
    listener:
      simple:
        acknowledge-mode: manual   #配置⼿动确认
  mail:
    host: smtp.qq.com       #需要在设置中开启 smtp
    username: 3471714098@qq.com  #发件⼈的邮箱
    password: sndvxlvhcfcidbbe      #邮箱的授权码, 并⾮个⼈密码
    port: 465  # SSL加密端口(必须用465,不能用25)
    protocol: smtp
    default-encoding: UTF-8
    properties:
      personal: "JQQ"
      mail:
        smtp:
          ssl:
            enable: true  # 强制启用SSL加密(关键配置)
          socketFactory:
            class: javax.net.ssl.SSLSocketFactory  # SSL连接工厂
          auth: true  # 启用认证

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
  file:
    name: user.log

---
#开发环境
spring:
  config:
    activate:
      on-profile: dev
nacos:
  address: 152.136.172.144:10020
mysql:
  password: "123456"
rabbitmq:
  addresses: 152.136.172.144:5672


---
#生产环境
spring:
  config:
    activate:
      on-profile: prod
nacos:
  address: 127.0.0.1:10020
mysql:
  password: Jqq200506,
rabbitmq:
  addresses: 127.0.0.1:5672

打包

检查是否添加打包插件

启动服务

分别启动三个服务

nohup java -jar -Dspring.profiles.active=prod blog-info-service-1.0-
SNAPSHOT.jar &
nohup java -jar -Dspring.profiles.active=prod user-info-service-1.0-
SNAPSHOT.jar &
nohup java -jar -Dspring.profiles.active=prod gateway-service-1.0-SNAPSHOT.jar
&

测试,服务启动完成后, 进⾏测试. 观察⽇志.

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值