SpringBoot响应式编程(3)R2DBC

news2024/12/26 12:48:24

一、概述

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);
                });
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2054824.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c++进阶——继承的定义,复杂的菱形继承及菱形虚拟继承

目录 前言&#xff1a; 1.继承的概念及定义 1.1继承的概念 1.2 继承定义 1.2.2继承关系和访问限定符 1.2.3继承基类成员访问方式的变化 2.基类和派生类对象赋值转换 3.继承中的作用域 4.派生类的默认成员函数 5.继承与友元 6. 继承与静态成员 7.复杂的菱形继承及菱…

AIoT智能物联网平台定义

随着科技的飞速发展&#xff0c;我们正步入一个由智能设备和互联网络构成的新时代。AIoT&#xff0c;即人工智能物联网&#xff08;Artificial Intelligence of Things&#xff09;&#xff0c;是这个时代的标志性产物。本文旨在探讨AIoT智能物联网平台的定义、核心组件、应用场…

wordpress站群搭建6外部库的引入和测试

wordpress站群搭建6wordpress外部库的引入和测试 本次目标我们主要讲解引入wordpress外部库并测试。 我们将一些外部库和工具包放入到pkg项目&#xff0c;方便其他项目引用。 1.安装go-wordpress库 进入到pkg项目 go get github.com/robbiet480/go-wordpress2.编写工具类 …

22 交换机双工模式

交换机双工模式 一、双工模式 &#xff08;一&#xff09;单工、半双工、全双 ​ 单工&#xff1a; ​ 只有一个信道&#xff0c;传输方向只能是单向的 ​ 半双工&#xff1a; ​ 只有一个信道&#xff0c;在同一时刻&#xff0c;只能是单向传输 ​ 全双工&#xff1a; …

【JavaEE】深入MyBatis:动态SQL操作与实战项目实现指南

目录 MyBatis的进阶操作动态SQL<if>标签<trim>标签<where>标签<set>标签<foreach>标签<include>标签 练习表白墙数据准备引⼊MyBatis 和 MySQL驱动依赖配置MySQL账号密码编写后端代码测试 图书管理系统数据库表设计引⼊MyBatis 和MySQL 驱…

阿里QuickBI实战

目录 引言数据采集、数据治理、数据分析举例资金在经济社会的流通性 概述数据分析的常见问题数据分析的痛点 QuickBI 产品架构图主功能链路数据建模与数据分析的关系使用场景1、搭建报表看板&#xff1a;仪表盘2、中国式报表工具&#xff1a;电子表格3、企业CXO驾驶舱&#xff…

网络工程3(子网通信,为什么要使用mac和ip)

文章目录 一. 子网如何通讯1. 子网内部通信2. 子网外部通信 二. 交换机和路由器的连接三. 为什么不只使用mac地址或ip地址进行网络通信1. 首先要明确的是&#xff0c;不管是只用mac或只用ip通信 四. 子网设备如何获得ip五. 不同网段的主机无法直接通信的原因 一. 子网如何通讯 …

GAMES101——作业7 路径追踪 (含提高:多线程,微平面理论)

任务 castRay(const Ray ray, int depth)in Scene.cpp: 在其中实现 Path Tracing 算法 可能用到的函数有&#xff1a; intersect(const Ray ray)in Scene.cpp: 求一条光线与场景的交点 sampleLight(Intersection pos, float pdf) in Scene.cpp: 在场景的所有光源上按面积…

用 CWE API 减轻软件产品中的安全风险

1. CWE REST API 推出的目的 8 月 8 号&#xff0c;CWE™ 计划推出了“CWE REST API”。 CWE™计划由美国网络安全与基础设施安全局(Cybersecurity & Infrastructure Security Agency(CISA))资助的国土安全系统工程与发展研究所(Homeland Security Systems Engineering a…

AutoSAR SecOC小小科普

目录 1.为什么要提SecOC 2.SecOC基本原理 2.1 参与到MAC计算的数据有哪些 2.2 新鲜度值如何管理 3.SecOC与各模块关联关系 1.为什么要提SecOC 在车载网络技术里&#xff0c;大家基本都是从CAN开始入门。在CAN DBC里&#xff0c;我们总能看到有些报文除了自带有效payload外…

AWS 消息队列服务 SQS

AWS 消息队列服务 SQS 引言什么是 SQSSQS 访问策略 Access Policy示例&#xff1a;如何为 DataLake Subscription 配置 SQS 引言 应用系统需要处理海量数据&#xff0c;数据发送方和数据消费方是通过什么方式来无缝集成消费数据的&#xff0c;AWS 提供 SQS 消息队列服务来解决…

42000 Star图标工具Mermaid!

Mermaid&#xff1a;用文本构建图表世界 - 精选真开源&#xff0c;释放新价值。 概览 Mermaid.js 是一个创新的开源工具&#xff0c;专为简化图表创建流程而设计。它通过一种简洁的文本描述语言&#xff0c;使得用户能够快速地生成流程图、序列图、甘特图等图表&#xff0c;而…

【威锋网-注册安全分析报告-无验证方式导致安全隐患】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

JavaEE过滤器的创建与使用过滤器的使用场景

过滤器 Filter也称之为过滤器&#xff0c;过滤器是javaEE规范肿定义的一种技术,可以让请求到达目标servlet之前,先进入到过滤器中,在过滤器中统一进行一些拦截处理,当处理完成后,可以继续向后执行,到达目标servlet,如果配置了多个过滤器,也可以进入下一个过滤器 创建过滤器 创…

一键更换Linux优质的软件源和docker源 —— 筑梦之路

一个非常牛逼的开源项目&#xff1a;https://github.com/SuperManito/LinuxMirrors.git LinuxMirrors 使换源更简单 - LinuxMirrors 支持的操作系统 系统名称适配版本Debian8.0 ~ 13Ubuntu14.04 ~ 24Kali Linux2.0 ~ 2024Linux Mint19.0 ~ 21 / LMDE 6DeepinallArmbianallP…

Redis7基础篇(一)

redis十大数据类型 目录 redis十大数据类型 redis键key 数据类型命令 redis字符串string 分布式锁 ​编辑 ​编辑​编辑应用场景 ​编辑​编辑 reids列表list 应用场景 redis哈希hash 应用场景 redis集合set 应用场景 redis有序集合zset&#xff08;sorted set集…

【设计模式】观察者模式和订阅发布模式

观察者模式 观察者模式包含观察目标和观察者两类对象。一个目标可以有任意数目的与之相依赖的观察者。一旦观察目标的状态发生改变&#xff0c;所有的观察者都将得到通知。 当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新&#xff0c;解决…

《机器学习》逻辑回归 梯度下降、混淆矩阵、随机种子、正则化惩罚 No.6

一、混淆矩阵 1、什么是混淆矩阵&#xff1f; 混淆矩阵是用于评估分类模型在不同类别上的预测准确性的工具。它提供了模型预测结果与真实结果之间的对应关系&#xff0c;帮助我们分析和理解模型的分类性能。 假设&#xff0c;要对15个人预测是否患病&#xff0c;使用1表示患病…

生产环境中MapReduce的最佳实践

目录 MapReduce跑的慢的原因 MapReduce常用调优参数 1. MapTask相关参数 2. ReduceTask相关参数 3. 总体调优参数 4. 其他重要参数 调优策略 MapReduce数据倾斜问题 1. 数据预处理 2. 自定义Partitioner 3. 调整Reduce任务数 4. 小文件问题处理 5. 二次排序 6. 使用…

【python与java的区别-03(集合、字典)】

一、Set python: 集合&#xff08;set&#xff09;是一个无序的不重复元素序列。 集合中的元素不会重复&#xff0c;并且可以进行交集、并集、差集等常见的集合操作。 可以使用大括号 { } 创建集合&#xff0c;元素之间用逗号 , 分隔&#xff0c; 或者也可以使用 set() 函数…