SpringBoot响应式编程(3)R2DBC

一、概述

1.1简介

R2DBC基于Reactive Streams反应流规范,它是一个开放的规范,为驱动程序供应商和使用方提供接口(r2dbc-spi),与JDBC的阻塞特性不同,它提供了完全反应式的非阻塞API关系型数据库交互。

简单说,R2DBC项目是支持使用反应式编程API访问关系型数据库的桥梁,定义统一接口规范,不同数据库厂家通过实现该规范提供驱动程序包。

  • R2DBC定义了所有数据存储驱动程序必须实现的SPI,目前实现R2DBC SPI的驱动程序包括:
  • r2dbc-h2:为H2实现的驱动程序;
  • r2dbc mariadb:为Mariadb实现的驱动程序;
  • r2dbc mssql:为Microsoft SQL Server实现的本机驱动程序;
  • r2dbc mysql:为Mysql实现的驱动程序;
  • r2dbc postgres:为PostgreSQL实现的驱动程序;

同时,r2dbc还提供反应式连接池r2dbc-pool(https://github.com/r2dbc/r2dbc-pool)。

相关文档

https://doc.qzxdp.cn/spring/spring-data-r2dbc.html

1.2R2DBC历史

首先大家要知道,我们最常使用的 JDBC 其实是同步的,而我们使用 WebFlux 的目的是为了通过异步的方式来提高服务端的响应效率,WebFlux 虽然实现了异步,但是由于 JDBC 还是同步的,而大部分应用都是离不开数据库的,所以其实效率本质上还是没有提升。

那么怎么办呢?有没有异步的 JDBC 呢?有!

目前市面上异步 JDBC 主要是两种:

  • ADAB:ADBA 是 Oracle 主导的 Java 异步数据库访问的标准 API,它将会集成于未来的 Java 标准发行版中。但是目前发展比较慢,只提供 OpenJDK 的沙盒特性供开发者研究之用。

  • R2DBC:R2DBC 是 Spring 官方在 Spring5 发布了响应式 Web 框架 Spring WebFlux 之后急需能够满足异步响应的数据库交互 API,不过由于缺乏标准和驱动,Pivotal 团队开始自己研究响应式关系型数据库连接 Reactive Relational Database Connectivity,并提出了 R2DBC 规范 API 用来评估可行性并讨论数据库厂商是否有兴趣支持响应式的异步非阻塞驱动程序。最早只有 PostgreSQL 、H2、MSSQL 三家数据库厂商,不过现在 MySQL 也加入进来了,这是一个极大的利好。目前 R2DBC 的最新版本是 0.9.0.RELEASE。

需要注意的是,这两个都不是对原来 JDBC 的补充,都是打算重新去设计数据库访问方案!

二、快速入门

2.1原生API使用

https://r2dbc.io

导入依赖

 <dependency>
            <groupId>io.asyncer</groupId>
            <artifactId>r2dbc-mysql</artifactId>
            <version>1.0.5</version>
        </dependency>

测试


        //0、MySQL配置
        MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder()
                .host("localhost")
                .port(3306)
                .username("root")
                .password("123456")
                .database("test")
                .build();

        //1、获取连接工厂
        MySqlConnectionFactory connectionFactory = MySqlConnectionFactory.from(configuration);


        //2、获取到连接,发送sql

        // JDBC: Statement: 封装sql的
        //3、数据发布者
        Mono.from(connectionFactory.create())
                .flatMapMany(connection ->
                        connection
                                .createStatement("select * from t_author where id=?id and name=?name")
                                .bind("id",1L) //具名参数
                                .bind("name","张三")
                                .execute()
                ).flatMap(result -> {
                    return result.map(readable -> {
                        Long id = readable.get("id", Long.class);
                        String name = readable.get("name", String.class);
                        return new TAuthor(id, name);
                    });
                })
                .subscribe(tAuthor -> System.out.println("tAuthor = " + tAuthor))
        ;

2.2Spring Data R2DBC整合

maven依赖

        <!-- https://mvnrepository.com/artifact/io.asyncer/r2dbc-mysql -->
        <dependency>
            <groupId>io.asyncer</groupId>
            <artifactId>r2dbc-mysql</artifactId>
            <version>1.0.5</version>
        </dependency>
        <!--        响应式 Spring Data R2dbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
        </dependency>

yml配置

spring:
  r2dbc:
    url: r2dbcs:mysql://:3306/2046204601
    username: 2046204601
    password: 2046204601
    pool:
      enabled: true
      initial-size: 1
      validation-query: select 1
  sql:
    init:
      mode: always
  jackson:
    default-property-inclusion: non_null # 序列化时忽略空属性值

logging:
  level:
    sql: debug
    web: debug
    com:
      example: debug
  pattern:
    console: '%-5level %C.%M[%line] - %msg%n'

my:
  secretkey: '636eac2534bcfcc0'

启动类

 * SpringBoot 对r2dbc的自动配置
 * 1、R2dbcAutoConfiguration:   主要配置连接工厂、连接池
 *
 * 2、R2dbcDataAutoConfiguration: 主要给用户提供了 R2dbcEntityTemplate 可以进行CRUD操作
 *      R2dbcEntityTemplate: 操作数据库的响应式客户端;提供CruD api ; RedisTemplate XxxTemplate
 *      数据类型映射关系、转换器、自定义R2dbcCustomConversions 转换器组件
 *      数据类型转换:int,Integer;  varchar,String;  datetime,Instant
 *
 *
 *
 * 3、R2dbcRepositoriesAutoConfiguration: 开启Spring Data声明式接口方式的CRUD;
 *      mybatis-plus: 提供了 BaseMapper,IService;自带了CRUD功能;
 *      Spring Data:  提供了基础的CRUD接口,不用写任何实现的情况下,可以直接具有CRUD功能;
 *
 *
 * 4、R2dbcTransactionManagerAutoConfiguration: 事务管理
 *
 */


@SpringBootApplication
public class R2DBCMainApplication {

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

测试

//1、Spring Data R2DBC,基础的CRUD用 R2dbcRepository 提供好了
    //2、自定义复杂的SQL(单表): @Query;
    //3、多表查询复杂结果集: DatabaseClient 自定义SQL及结果封装;


    //Spring Data 提供的两个核心底层组件

    @Autowired  // join查询不好做; 单表查询用
    R2dbcEntityTemplate r2dbcEntityTemplate; //CRUD API; 更多API操作示例: https://docs.spring.io/spring-data/relational/reference/r2dbc/entity-persistence.html


    @Autowired  //贴近底层,join操作好做; 复杂查询好用
    DatabaseClient databaseClient; //数据库客户端



    @Autowired// 导入R2dbcCustomConversions类,用于自定义R2DBC的转换器
    R2dbcCustomConversions r2dbcCustomConversions;

    @Test
    void r2dbcEntityTemplate() throws IOException {

        // Query By Criteria: QBC

        //1、Criteria构造查询条件  where id=1 and name=张三
        Criteria criteria = Criteria
                .empty()
                .and("id").is(1L)
                .and("name").is("张三");

        //2、封装为 Query 对象
        Query query = Query.query(criteria);


        r2dbcEntityTemplate
                .select(query, TAuthor.class)
                .subscribe(tAuthor -> System.out.println("tAuthor = " + tAuthor.getName()));

        System.in.read();
    }
    @Test
    void databaseClient() throws IOException {

        // 底层操作
        databaseClient
                .sql("select * from t_author")
//                .bind(0,2L)
                .fetch() //抓取数据
                .all()//返回所有
                .map(map -> {  //map == bean  属性=值
                    System.out.println("map = " + map);
                    String id = map.get("id").toString();
                    String name = map.get("name").toString();
                    return new TAuthor(Long.parseLong(id), name, null);
                })
                .subscribe(tAuthor -> System.out.println("tAuthor = " + tAuthor));
        System.in.read();


    }

Repository

@EnableR2dbcRepositories //开启 R2dbc 仓库功能;jpa
@Configuration
public class R2DbcConfiguration {


}
@Repository
public interface AuthorRepositories extends R2dbcRepository<TAuthor,Long> {

    //默认继承了一堆CRUD方法; 像mybatis-plus

    //QBC: Query By Criteria
    //QBE: Query By Example

    //成为一个起名工程师  where id In () and name like ?
    //仅限单表复杂条件查询
    Flux<TAuthor> findAllByIdInAndNameLike(Collection<Long> id, String name);

    //多表复杂查询

    @Query("select * from t_author") //自定义query注解,指定sql语句
    Flux<TAuthor> findHaha();


    // 1-1:关联
    // 1-N:关联
    //场景:
    // 1、一个图书有唯一作者; 1-1
    // 2、一个作者可以有很多图书: 1-N



}

2.3一对一操作

转换器

@ReadingConverter //读取数据库数据的时候,把row转成 TBook
public class BookConverter implements Converter<Row, TBookAuthor> {

    //1)、@Query 指定了 sql如何发送
    //2)、自定义 BookConverter 指定了 数据库返回的一 Row 数据,怎么封装成 TBook
    //3)、配置 R2dbcCustomConversions 组件,让 BookConverter 加入其中生效
    @Override
    public TBookAuthor convert(Row source) {
        if(source == null) return null;
        //自定义结果集的封装
        TBookAuthor tBook = new TBookAuthor();

        tBook.setId(source.get("id", Long.class));
        tBook.setTitle(source.get("title", String.class));

        Long author_id = source.get("author_id", Long.class);
        tBook.setAuthorId(author_id);
        tBook.setPublishTime(source.get("publish_time", Instant.class));


        //让 converter兼容更多的表结构处理
        if (source.getMetadata().contains("name")) {
            TAuthor tAuthor = new TAuthor();
            tAuthor.setId(author_id);
            tAuthor.setName(source.get("name", String.class));

            tBook.setAuthor(tAuthor);
        }



        return tBook;
    }

配置生效

@EnableR2dbcRepositories //开启 R2dbc 仓库功能;jpa
@Configuration
public class R2DbcConfiguration {


    @Bean //替换容器中原来的
    @ConditionalOnMissingBean
    public R2dbcCustomConversions conversions(){

        //把我们的转换器加入进去; 效果新增了我们的 Converter
        return R2dbcCustomConversions.of(MySqlDialect.INSTANCE,new BookConverter());
    }
}

自定义 Converter<Row,Bean> 方式

    @Bean
    R2dbcCustomConversions r2dbcCustomConversions(){
        List<Converter<?, ?>> converters = new ArrayList<>();
        converters.add(new BookConverter());
        return R2dbcCustomConversions.of(MySqlDialect.INSTANCE, converters);
    }

//1-1: 结合自定义 Converter
bookRepostory.hahaBook(1L)
        .subscribe(tBook -> System.out.println("tBook = " + tBook));

编程式封装方式: 使用DatabaseClient

//1-1:第二种方式
databaseClient.sql("select b.*,t.name as name from t_book b " +
                "LEFT JOIN t_author t on b.author_id = t.id " +
                "WHERE b.id = ?")
        .bind(0, 1L)
        .fetch()
        .all()
        .map(row-> {
            String id = row.get("id").toString();
            String title = row.get("title").toString();
            String author_id = row.get("author_id").toString();
            String name = row.get("name").toString();
            TBook tBook = new TBook();

            tBook.setId(Long.parseLong(id));
            tBook.setTitle(title);

            TAuthor tAuthor = new TAuthor();
            tAuthor.setName(name);
            tAuthor.setId(Long.parseLong(author_id));

            tBook.setAuthor(tAuthor);

            return tBook;
        })
        .subscribe(tBook -> System.out.println("tBook = " + tBook));

2.4一对多操作

bufferUntilChanged 

bufferUntilChanged 是一个操作符,用于在数据流中缓存元素,直到遇到一个与前一个元素不同的元素。

        Flux.just(1,2,3,4,8,5,6,7,8,9,10)
                .bufferUntilChanged(integer -> integer%4==0 )
                .subscribe(list-> System.out.println("list = " + list));

1-N

@Table("t_author")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Data
public class TAuthor {

    @Id
    private Long id;
    private String name;

//    //1-N如何封装
    @Transient //临时字段,并不是数据库表中的一个字段
//    @Field(exist=false)
    private List<TBook> books;


}
    @Test
    void oneToN() throws IOException {

//        databaseClient.sql("select a.id aid,a.name,b.* from t_author a  " +
//                "left join t_book b on a.id = b.author_id " +
//                "order by a.id")
//                .fetch()
//                .all(row -> {
//
//                })


        // 1~6
        // 1:false 2:false 3:false 4: true 8:true 5:false 6:false 7:false 8:true 9:false 10:false
        // [1,2,3]
        // [4,8]
        // [5,6,7]
        // [8]
        // [9,10]
        // bufferUntilChanged:
        // 如果下一个判定值比起上一个发生了变化就开一个新buffer保存,如果没有变化就保存到原buffer中

//        Flux.just(1,2,3,4,8,5,6,7,8,9,10)
//                .bufferUntilChanged(integer -> integer%4==0 )
//                .subscribe(list-> System.out.println("list = " + list));
        ; //自带分组


        Flux<TAuthor> flux = databaseClient.sql("select a.id aid,a.name,b.* from t_author a  " +
                        "left join t_book b on a.id = b.author_id " +
                        "order by a.id")
                .fetch()
                .all()
                .bufferUntilChanged(rowMap -> Long.parseLong(rowMap.get("aid").toString()))
                .map(list -> {
                    TAuthor tAuthor = new TAuthor();
                    Map<String, Object> map = list.get(0);
                    tAuthor.setId(Long.parseLong(map.get("aid").toString()));
                    tAuthor.setName(map.get("name").toString());


                    //查到的所有图书
                    List<TBook> tBooks = list.stream()
                            .map(ele -> {
                                TBook tBook = new TBook();

                                tBook.setId(Long.parseLong(ele.get("id").toString()));
                                tBook.setAuthorId(Long.parseLong(ele.get("author_id").toString()));
                                tBook.setTitle(ele.get("title").toString());
                                return tBook;
                            })
                            .collect(Collectors.toList());

                    tAuthor.setBooks(tBooks);
                    return tAuthor;
                });//Long 数字缓存 -127 - 127;// 对象比较需要自己写好equals方法



        flux.subscribe(tAuthor -> System.out.println("tAuthor = " + tAuthor));

        System.in.read();


    }

2.5 route + handler

此时就可以调用封装好的 CRUD 方法进行简单的增删改查操作了。在 Webflux 框架中,我们可以使用 SpringMVC 中 Controller + Service 的模式进行开发,也可以使用 Webflux 中 route + handler 的模式进行开发。

handler 就相当于定义很多处理器,其中不同的方法负责处理不同路由的请求,其对应的是传统的 Service 层

@Component
public class UserHandler {

    @Autowired
    private UserRepository userRepository;

    public Mono<ServerResponse> addUser(ServerRequest request) {
        return ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(userRepository.saveAll(request.bodyToMono(User.class)), User.class);
    }

    public Mono<ServerResponse> delUser(ServerRequest request) {
        return userRepository.findById(Integer.parseInt(request.pathVariable("id")))
                .flatMap(user -> userRepository.delete(user).then(ServerResponse.ok().build()))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> updateUser(ServerRequest request) {
        return ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(userRepository.saveAll(request.bodyToMono(User.class)), User.class);
    }

    public Mono<ServerResponse> getAllUser(ServerRequest request) {
        return ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(userRepository.findAll(), User.class);
    }

    public Mono<ServerResponse> getAllUserStream(ServerRequest request) {
        return ServerResponse.ok()
                .contentType(MediaType.TEXT_EVENT_STREAM)
                .body(userRepository.findAll(), User.class);
    }

}

route 就是路由配置,其规定路由的分发规则,将不同的请求路由分发给相应的 handler 进行业务逻辑的处理,其对应的就是传统的 Controller 层

@Configuration
public class RouteConfig {

    @Bean
    RouterFunction<ServerResponse> userRoute(UserHandler userHandler) {
        return RouterFunctions.nest(
                RequestPredicates.path("/userRoute"),
                RouterFunctions.route(RequestPredicates.POST(""), userHandler::addUser)
                        .andRoute(RequestPredicates.DELETE("/{id}"), userHandler::delUser)
                        .andRoute(RequestPredicates.PUT(""), userHandler::updateUser)
                        .andRoute(RequestPredicates.GET(""), userHandler::getAllUser)
                        .andRoute(RequestPredicates.GET("/stream"), userHandler::getAllUserStream)
        );
    }

}

三、R2DBC 实战

3.1环境配置

maven依赖

 <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-crypto</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.4.0</version>
        </dependency>

yml依赖

spring:
  r2dbc:
    url: r2dbcs:mysql://:3306/2046204601
    username: 
    password: 
    pool:
      enabled: true
      initial-size: 1
      validation-query: select 1
  sql:
    init:
      mode: always
  jackson:
    default-property-inclusion: non_null # 序列化时忽略空属性值

logging:
  level:
    sql: debug
    web: debug
    com:
      example: debug
  pattern:
    console: '%-5level %C.%M[%line] - %msg%n'

my:
  secretkey: '636eac2534bcfcc0'

schema.sql

create table if not exists `user_react`
(
    id          char(19)    not null primary key,
    name        varchar(10) not null,
    account     varchar(15) not null,
    password    varchar(65) not null,
    role        char(5)     not null,
    insert_time datetime    not null default current_timestamp,
    update_time datetime    not null default current_timestamp on update current_timestamp,

    unique (account),
    index (role)
);

3.2CRUD

实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserReact {
    public static final String ROLE_USER = "hOl7U";
    public static final String ROLE_ADMIN = "yxp4r";
    @Id
    @CreatedBy
    private String id;
    private String name;
    private String account;
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String password;
    @JsonIgnore
    private String role;
    @ReadOnlyProperty
    private LocalDateTime insertTime;
    @ReadOnlyProperty
    private LocalDateTime updateTime;
}

Repository

@Repository
public interface UserRepository extends ReactiveCrudRepository<UserReact, String> {

    Mono<UserReact> findByAccount(String account);

    @Query("""
            select * from user_react u where u.role=:role;
            """)
    Flux<UserReact> findByRole(String role);
}

工具类

@Component // 标记为Spring组件,使其可以被自动扫描并注入到其他类中
@Slf4j // 使用Lombok库提供的日志功能,简化日志记录操作
public class JWTComponent {
    // 私钥,用于签名和验证JWT令牌
    @Value("${my.secretkey}")
    private String secretkey;

    /**
     * 对给定的负载数据进行编码,生成一个JWT令牌。
     * @param map 包含有效载荷数据的Map对象
     * @return 返回编码后的JWT令牌字符串
     */
    public String encode(Map<String, Object> map) {
        // 设置令牌过期时间为当前时间加一个月
        LocalDateTime time = LocalDateTime.now().plusMonths(1);
        return JWT.create()
                .withPayload(map) // 添加有效载荷数据
                .withIssuedAt(new Date()) // 设置令牌签发时间
                .withExpiresAt(Date.from(time.atZone(ZoneId.systemDefault()).toInstant())) // 设置令牌过期时间
                .sign(Algorithm.HMAC256(secretkey)); // 使用HMAC256算法和私钥进行签名
    }

    /**
     * 解码给定的JWT令牌,验证其有效性并返回解码后的有效载荷。
     * @param token 要解码的JWT令牌字符串
     * @return 返回一个包含解码后有效载荷的Mono对象
     */
    public Mono<DecodedJWT> decode(String token) {
        try {
            // 使用指定的算法和私钥验证并解码JWT令牌
            DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secretkey)).build().verify(token);
            return Mono.just(decodedJWT); // 如果验证成功,返回解码后的有效载荷
        } catch (TokenExpiredException | SignatureVerificationException | JWTDecodeException e) {
            Code code = Code.FORBIDDEN; // 默认错误代码为禁止访问
            if (e instanceof TokenExpiredException) {
                code = Code.TOKEN_EXPIRED; // 如果令牌已过期,则设置相应的错误代码
            }
            return Mono.error(XException.builder().code(code).build()); // 返回一个包含错误信息的Mono对象
        }
    }
}
@Configuration
public class PasswordEncoderConfig {
    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

异常

@Getter
public enum Code {
    LOGIN_ERROR(400, "用户名密码错误"),
    BAD_REQUEST(400, "请求错误"),
    UNAUTHORIZED(401, "未登录"),
    TOKEN_EXPIRED(403, "过期请重新登录"),
    FORBIDDEN(403, "无权限");
    public static final int ERROR = 400;
    private final int code;
    private final String message;

    Code(int code, String message) {
        this.code = code;
        this.message = message;
    }

}
@EqualsAndHashCode(callSuper = true)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class XException extends RuntimeException{
    private Code code;
    private int codeN;
    private String message;
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import reactor.core.publisher.Mono;

@Slf4j
@RestControllerAdvice
public class ExceptionController {
    // 处理XException异常,用于Mono中异常的处理
    // 注意:此方法在filter内无效,需要在单独处理。
    @ExceptionHandler(XException.class)
    public Mono<ResultVO> handleXException(XException exception) {
        // 如果异常中有错误码,则返回带有错误码的错误信息
        if(exception.getCode() != null) {
            return Mono.just(ResultVO.error(exception.getCode()));
        }
        // 否则,返回带有错误码和错误信息的默认错误信息
        return Mono.just(ResultVO.error(exception.getCodeN(), exception.getMessage()));
    }

    // 处理通用的Exception异常
    @ExceptionHandler(Exception.class)
    public Mono<ResultVO> handleException(Exception exception) {
        // 返回带有BAD_REQUEST错误码和异常信息的错误信息
        return Mono.just(ResultVO.error(Code.BAD_REQUEST.getCode(), exception.getMessage()));
    }

    // 处理UncategorizedR2dbcException异常,通常与数据库操作相关
    @ExceptionHandler(UncategorizedR2dbcException.class)
    public Mono<ResultVO> handelUncategorizedR2dbcException(UncategorizedR2dbcException exception) {
        // 返回带有BAD_REQUEST错误码和"唯一约束冲突!"加上异常信息的错误信息
        return Mono.just(ResultVO.error(Code.BAD_REQUEST.getCode(), "唯一约束冲突!" + exception.getMessage()));
    }
}

vo层

public interface RequestConstant {
    String TOKEN = "token";
    String UID = "uid";
    String ROLE = "role";
}

服务类

// 使用@Service注解,将该类标记为Spring框架的服务组件
@Service
// 使用@Slf4j注解,自动为该类生成一个SLF4J日志记录器
@Slf4j
// 使用@RequiredArgsConstructor注解,自动生成一个构造函数,用于初始化final字段
@RequiredArgsConstructor
public class InitService {
    // 注入UserService依赖
    private final UserService userService;

    // 使用@Transactional注解,确保方法内的操作在一个事务中执行
    @Transactional
    // 使用@EventListener注解,监听ApplicationReadyEvent事件,当事件发生时执行该方法
    @EventListener(classes = ApplicationReadyEvent.class)
    public Mono<Void> onApplicationReadyEvent() {
        // 定义一个账户名
        String account = "admin";
        // 调用userService的getUser方法,尝试获取指定账户的用户信息
        return userService.getUser(account)
                // 如果用户不存在(返回的Mono为空),则执行以下操作
                .switchIfEmpty(Mono.defer(() -> {
                    // 创建一个新的UserReact对象,设置相关属性
                    UserReact user = UserReact.builder()
                            .name(account)
                            .account(account)
                            .role(UserReact.ROLE_ADMIN)
                            .build();
                    // 调用userService的addUser方法,添加新用户并返回其Mono
                    return userService.addUser(user);
                })).then(); // 最后返回一个完成的Mono<Void>
    }
}
// 导入相关依赖和服务注解
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Mono;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

// 定义UserService类,用于处理用户相关的业务逻辑
@Service
@Slf4j
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository; // 注入UserRepository,用于访问数据库中的用户数据
    private final PasswordEncoder passwordEncoder; // 注入PasswordEncoder,用于密码加密

    // 根据账号获取用户信息的方法
    public Mono<UserReact> getUser(String account) {
        return userRepository.findByAccount(account); // 调用userRepository的findByAccount方法查询用户
    }

    // 根据用户ID获取用户信息的方法
    public Mono<UserReact> getUserById(String uid) {
        return userRepository.findById(uid); // 调用userRepository的findById方法查询用户
    }

    // 添加用户的方法,使用事务注解确保操作的原子性
    @Transactional
    public Mono<UserReact> addUser(UserReact user) {
        return userRepository.findByAccount(user.getAccount()) // 先检查账号是否已存在
                .handle((u, sink) ->
                        sink.error(XException.builder() // 如果已存在,则抛出异常
                                .codeN(Code.ERROR)
                                .message("用户已存在")
                                .build())
                )
                .cast(UserReact.class) // 将结果转换为UserReact类型
                .switchIfEmpty(Mono.defer(() -> { // 如果不存在,则创建新用户
                    user.setPassword(passwordEncoder.encode(user.getAccount())); // 对密码进行加密
                    return userRepository.save(user); // 保存用户到数据库
                }));
    }

    // 根据角色获取用户列表的方法
    public Mono<List<UserReact>> listUsers(String role) {
        return userRepository.findByRole(role).collectList(); // 调用userRepository的findByRole方法查询用户列表并收集为List
    }
}

控制层

// 定义一个名为LoginController的类,用于处理登录相关的请求
@RestController
@Slf4j // 使用Lombok库提供的日志功能
@RequiredArgsConstructor // 使用Lombok库提供的构造器注入功能
@RequestMapping("/api/") // 设置该控制器处理的请求的基本路径为"/api/"
public class LoginController {
    private final UserService userService; // 用户服务组件,用于获取用户信息
    private final PasswordEncoder passwordEncoder; // 密码编码器,用于验证密码是否正确
    private final JWTComponent jwtComponent; // JSON Web Token组件,用于生成和解析JWT令牌

    // 处理POST请求,映射到"/login"路径,用于用户登录
    @PostMapping("login")
    public Mono<ResultVO> login(@RequestBody UserReact user, ServerHttpResponse response) {
        // 从userService中获取用户信息,根据用户的账号进行筛选
        return userService.getUser(user.getAccount())
                // 检查用户提供的密码是否与数据库中的密码匹配
                .filter(u -> passwordEncoder.matches(user.getPassword(), u.getPassword()))
                // 如果密码匹配成功,则执行以下操作
                .map(u -> {
                    // 创建一个包含用户ID和角色信息的Map对象
                    Map<String, Object> tokenM = Map.of(
                            RequestConstant.UID, u.getId(),
                            RequestConstant.ROLE, u.getRole());
                    // 使用jwtComponent对tokenM进行编码,生成JWT令牌
                    String token = jwtComponent.encode(tokenM);
                    // 获取响应头对象
                    HttpHeaders headers = response.getHeaders();
                    // 将生成的JWT令牌添加到响应头的"token"字段中
                    headers.add("token", token);
                    // 将用户的角色添加到响应头的"role"字段中
                    headers.add("role", u.getRole());
                    // 返回一个表示成功的ResultVO对象,其中包含用户信息
                    return ResultVO.success(Map.of("user", u));
                })
                // 如果密码不匹配或用户不存在,则返回一个表示登录错误的ResultVO对象
                .defaultIfEmpty(ResultVO.error(Code.LOGIN_ERROR));
    }
}
// 定义一个名为AdminController的控制器类,用于处理与管理员相关的API请求
@RestController
// 使用Slf4j注解,为该类提供日志记录功能
@Slf4j
// 使用RequiredArgsConstructor注解,自动生成构造函数,要求所有final字段都必须被初始化
@RequiredArgsConstructor
// 设置该控制器的基础URL路径为"/api/admin/"
@RequestMapping("/api/admin/")
public class AdminController {

    // 注入UserService实例,用于处理用户相关的业务逻辑
    private final UserService userService;

    // 处理POST请求,创建新用户
    @PostMapping("users")
    public Mono<ResultVO> postUsers(@RequestBody UserReact user) {
        // 调用userService的addUser方法添加用户,并返回一个包含成功信息的ResultVO对象
        return userService.addUser(user)
                .thenReturn(ResultVO.ok());
    }

    // 处理GET请求,获取用户信息
    @GetMapping("info")
    public Mono<ResultVO> getInfo(@RequestAttribute(RequestConstant.UID) String uid) {
        // 调用userService的getUserById方法根据用户ID获取用户信息,并将其包装在ResultVO对象中返回
        return userService.getUserById(uid)
                .map(user -> ResultVO.success(Map.of("user", user)));
    }
}

过滤器

// 定义一个名为ResponseHelper的类,用于处理响应
@Component
@Slf4j
@RequiredArgsConstructor
public class ResponseHelper {
    // 使用ObjectMapper对象进行JSON序列化
    private final ObjectMapper objectMapper;

    // 定义一个response方法,接收Code枚举类型和一个ServerWebExchange对象作为参数
    @SneakyThrows
    public Mono<Void> response(Code code, ServerWebExchange exchange) {
        // 将错误信息转换为JSON字符串并编码为UTF-8字节数组
        byte[] bytes = objectMapper.writeValueAsString(ResultVO.error(code))
                .getBytes(StandardCharsets.UTF_8);
        // 获取服务器响应对象
        ServerHttpResponse response = exchange.getResponse();
        // 将字节数组包装成DataBuffer对象
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        // 设置响应内容类型为JSON
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        // 将DataBuffer写入响应并返回Mono<Void>对象
        return response.writeWith(Flux.just(wrap));
    }
}
// 导入相关依赖
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

// 定义一个登录过滤器类,实现WebFilter接口
@Component
@Slf4j
@Order(1)
@RequiredArgsConstructor
public class LoginFilter implements WebFilter {
    // 定义需要过滤的路径模式
    private final PathPattern path = new PathPatternParser().parse("/api/**");
    // 定义不需要过滤的路径模式列表
    private final List<PathPattern> excludesS = List.of(new PathPatternParser().parse("/api/login"));
    // 注入JWT组件
    private final JWTComponent jwtComponent;
    // 注入响应帮助类
    private final ResponseHelper responseHelper;

    // 重写filter方法
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        // 获取请求对象
        ServerHttpRequest request = exchange.getRequest();
        // 遍历排除列表,如果请求路径匹配排除列表中的任何一个,直接放行
        for (PathPattern p : excludesS) {
            if (p.matches(request.getPath().pathWithinApplication())) {
                return chain.filter(exchange);
            }
        }
        // 如果请求路径不在过滤范围内,返回异常响应
        if (!path.matches(request.getPath().pathWithinApplication())) {
            return responseHelper.response(Code.BAD_REQUEST, exchange);
        }
        // 从请求头中获取token
        String token = request.getHeaders().getFirst(RequestConstant.TOKEN);
        // 如果token为空,返回未授权响应
        if (token == null) {
            return responseHelper.response(Code.UNAUTHORIZED, exchange);
        }
        // 解码token,并将解码结果放入请求属性中
        return jwtComponent.decode(token)
                .flatMap(decode -> {
                    Map<String, Object> attributes = exchange.getAttributes();
                    attributes.put(RequestConstant.UID, decode.getClaim(RequestConstant.UID).asString());
                    attributes.put(RequestConstant.ROLE, decode.getClaim(RequestConstant.ROLE).asString());
                    // 继续执行后续过滤器链
                    return chain.filter(exchange);
                });
    }
}

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烟雨平生9527

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值