SpringBoot响应式编程(3)R2DBC

2024-08-20 21:52

本文主要是介绍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/2046204601username: 2046204601password: 2046204601pool:enabled: trueinitial-size: 1validation-query: select 1sql:init:mode: alwaysjackson:default-property-inclusion: non_null # 序列化时忽略空属性值logging:level:sql: debugweb: debugcom:example: debugpattern: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;@Testvoid 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();}
    @Testvoid 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 加入其中生效@Overridepublic 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 //替换容器中原来的@ConditionalOnMissingBeanpublic R2dbcCustomConversions conversions(){//把我们的转换器加入进去; 效果新增了我们的 Converterreturn R2dbcCustomConversions.of(MySqlDialect.INSTANCE,new BookConverter());}
}

自定义 Converter<Row,Bean> 方式

    @BeanR2dbcCustomConversions 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 {@Idprivate Long id;private String name;//    //1-N如何封装@Transient //临时字段,并不是数据库表中的一个字段
//    @Field(exist=false)private List<TBook> books;}
    @Testvoid 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 {@Autowiredprivate 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 {@BeanRouterFunction<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/2046204601username: password: pool:enabled: trueinitial-size: 1validation-query: select 1sql:init:mode: alwaysjackson:default-property-inclusion: non_null # 序列化时忽略空属性值logging:level:sql: debugweb: debugcom:example: debugpattern: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@CreatedByprivate String id;private String name;private String account;@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)private String password;@JsonIgnoreprivate String role;@ReadOnlyPropertyprivate LocalDateTime insertTime;@ReadOnlyPropertyprivate 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 {@Beanpublic 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方法,添加新用户并返回其Monoreturn 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方法查询用户}// 添加用户的方法,使用事务注解确保操作的原子性@Transactionalpublic 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对象作为参数@SneakyThrowspublic 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);// 设置响应内容类型为JSONresponse.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方法@Overridepublic 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);}// 从请求头中获取tokenString 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);});}
}

这篇关于SpringBoot响应式编程(3)R2DBC的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1091280

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor