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

什么是私有继承
私有,公有,针对类而言; 私有( private )的成员,自己的,只能在自己内部( 类的定义体内部 )访问,外部( 类的定义体外部 )不能访问/调用; 公有( 或者说公开,public )的成员࿰…...

Scratch编程:开启智能硬件控制的大门
标题:“Scratch编程:开启智能硬件控制的大门” 在当今数字化时代,编程不仅仅是与计算机的交互,更是与物理世界的连接。Scratch,这款由麻省理工学院媒体实验室开发的视觉化编程语言,以其易学易用的特性&…...
机器学习第十二章-计算学习理论
目录 12.1基础知识 12.2 PAC学习 12.3有限假设空间 12.3.1可分情形 12.3.2不可分情形 12.4VC维 12.5 Rademacher复杂度 12.1基础知识 计算学习理论研究的是关于通过"计算"来进行"学习"的理论,即关于机器学习的理论基础,其目的…...

Java-自定义注解操作日志记录处理(@Pointcut注解不是必须的)
在Java中,使用自定义注解结合Spring AOP来实现操作日志记录是一种常见的做法。这种方式可 以帮助你轻松地在不修改业务代码的情况下增加日志记录的功能。 下面我将详细介绍如何定义一个自定义注解,并结合Spring AOP来实现操作日志记录的功能。 1. 定义自定义注解 首先,我…...

【c++】深入理解别名机制--引用
🌟🌟作者主页:ephemerals__ 🌟🌟所属专栏:C 目录 前言 一、引用的概念和定义 二、引用的特性 三、引用的实用性 1.引用传参 2.引用做返回值 2.1 引用做返回值的作用 2.2 引用坍缩问题、悬挂引用问…...

简便的qemu img扩容方法
虚拟机用着用着磁盘空间就不够了,那就要想办法增加磁盘空间大小 了。在虚拟机本身磁盘的基础上直接增加空间大小最简便,于是记录一下方法。 首先,在虚拟机关机状态下,使用qemu-img命令给虚拟机的磁盘镜像增加虚拟空间5GBÿ…...

EPERM: operation not permitted,
这个错误提示 EPERM: operation not permitted, mkdir C:\Program Files\nodejs\node_global\node_modules\pnpm_tmp 通常是因为权限不足导致的。在 Windows 系统中,C:\Program Files\ 目录通常需要管理员权限才能写入。 要解决这个问题,你可以尝试以下…...

将Centos 8 Linux内核版本升级或降级到指定版本
本文以centos 8.0为例,内核版本为4.18.0-80.el8.x86_64,升级到内核版本为4.18.0-80.4.2.el8_0.x86_64。 1.查看当前系统版本信息 [rootcentos80-1905 ~]# uname -sr Linux 4.18.0-80.el8.x86_642.在网站:https://vault.centos.org/里面下载…...

小程序商城被盗刷,使用SCDN安全加速有用吗?
在电子商务蓬勃发展的今天,小程序商城因其便捷性和灵活性成为商家和消费者的新宠。然而,随着其普及,小程序商城的安全问题也日益凸显,尤其是盗刷现象频发,给商家和用户带来了巨大损失。面对这一挑战,是否可…...

nginx的基本使用与其日志
文章目录 1.nginx编译安装脚本2.nginx平滑升级,以及其步骤3.nginx核心配置,及实现nginx多虚拟主机4.nginx日志格式定制5.nginx反向代理及https安全加密6.基于LNMP和Redis的phpmyadmin的会话保持,以及其完整步骤 1.nginx编译安装脚本 #编译安…...

linux | 苹果OpenCL(提高应用软件如游戏、娱乐以及科研和医疗软件的运行速度和响应)
点击上方"蓝字"关注我们 01、引言 >>> OpenCL 1.0 于 2008 年 11 月发布。 OpenCL 是为个人电脑、服务器、移动设备以及嵌入式设备的多核系统提供并行编程开发的底层 API。OpenCL 的编程语言类似于 C 语言。其可以用于包含 CPU、GPU 以及来自主流制造商如 …...

算法-UKF中Sigma点生成
void UKF::MakeSigmaPoints() {Eigen::VectorXd x_aug_ Eigen::VectorXd(n_x_);x_aug_.head(n_x_) x_;Eigen::MatrixXd P_aug Eigen::MatrixXd::Zero(n_x_, n_x_);// 转成正定矩阵P_aug pdefinite_svd(P_);// LLT分解Eigen::MatrixXd L P_aug.llt().matrixL();sigma_point…...

精选五款热门骨传导耳机分享,让你避免踩坑的陷阱
因为骨传导耳机独特的佩戴方式和声音的传播方式,受到了小耳、油耳以及运动爱好者的的喜爱,但也由于市面上的骨传导耳机品牌越来越多,很多朋友不知道该怎么选择,今天我挑选出市面上体验感较好,各方面比较出色的骨传导给…...

「字符串」前缀函数|KMP匹配:规范化next数组 / LeetCode 28(C++)
概述 为什么大家总觉得KMP难?难的根本就不是这个算法本身。 在互联网上你可以见到八十种KMP算法的next数组定义和模式串回滚策略,把一切都懂得特别混乱。很多时候初学者的难点根本不在于这个算法本身,而是它令人痛苦的百花齐放的定义。 有…...

python人工智能002:jupyter基本使用
小知识:将jupyter修改为中文,修改用户变量, 注意是用户变量,不是系统变量 新增用户变量 变量名:LANG 变量值:zh_CN.UTF8 然后重启jupyter 上一章的软件安装完成之后,就可以创建文件夹来学习写…...

Linux使用 firewalld管理防火墙命令
Linux 发行版中使用的动态防火墙管理工具。使用 firewalld,你可以查看防火墙状态、当前配置的规则以及开放的端口。以下是一些常用的 firewalld 命令来管理和查看防火墙状态及端口配置。 1. 查看防火墙状态 检查 firewalld 是否正在运行 sudo systemctl status f…...

二叉树(三)
一、二叉树的遍历 二叉树遍历是按照某种特定的规则,依次对二叉树中的结点进行相应的操作,并且每个结点只操作一次。 1.前序遍历(先根遍历) 前序遍历(Preorder Traversal也叫先序遍历)——根、左子树、右…...

05--kubernetes组件与安装
前言:终于写到kubernetes(k8s),容器编排工具不止k8s一个,它的优势在于搭建集群,也是传统运维和云计算运维的第一道门槛,这里会列出两种安装方式,详细步骤会在下文列出,文…...

EmguCV学习笔记 VB.Net和C# 下的OpenCv开发 C# 目录
版权声明:本文为博主原创文章,转载请在显著位置标明本文出处以及作者网名,未经作者允许不得用于商业目的。 EmguCV是一个基于OpenCV的开源免费的跨平台计算机视觉库,它向C#和VB.NET开发者提供了OpenCV库的大部分功能。 教程VB.net版本请访问…...

探索TensorFlow:深度学习的未来
标题:探索TensorFlow:深度学习的未来 在当今快速发展的人工智能领域,TensorFlow无疑是最耀眼的明珠之一。TensorFlow是由Google Brain团队开发的一个开源机器学习框架,它以其强大的灵活性、易用性和高效的性能,迅速成…...

探索地理空间分析的新世界:Geopandas的魔力
文章目录 探索地理空间分析的新世界:Geopandas的魔力背景:为何选择Geopandas?这个库是什么?如何安装这个库?五个简单的库函数使用方法场景应用:Geopandas在实际工作中的应用常见bug及解决方案总结 探索地理…...

如何为网站申请免费SSL证书?
一、准备阶段 确定证书类型: 对于大多数个人博客和小型企业网站,DV(域名验证)SSL证书已足够使用,因为它仅验证域名所有权,成本较低且验证快速。准备域名: 确保你拥有一个有效的域名,…...

Java项目集成RocketMQ
文章目录 1.调整MQ的配置1.进入bin目录2.关闭broker和namesrv3.查看进程确认关闭4.编辑配置文件broker.conf,配置brokerIP15.开放端口109116.重新启动1.进入bin目录2.启动mqnamesrv和mqbroker1.启动 NameServer 并将输出重定向到 mqnamesrv.log2.**启动 Broker 并将…...

如何将 Bamboo agent 能力迁移到极狐GitLab tag 上?
极狐GitLab 是 GitLab 在中国的发行版,专门面向中国程序员和企业提供企业级一体化 DevOps 平台,用来帮助用户实现需求管理、源代码托管、CI/CD、安全合规,而且所有的操作都是在一个平台上进行,省事省心省钱。可以一键安装极狐GitL…...

正则表达式入门:Python ‘ re ‘ 模块详解
正则表达式(Regular Expression,简称 re)是一种强大而灵活的工具,广泛用于字符串匹配、替换和分割等操作,尤其在处理网页爬虫数据时非常有用。Python 提供了 " re " 模块来支持正则表达式的使用,…...

thinkphp8.0+aliapy(支付宝)pc网站支付
环境:宝塔-centOS8.5,php8.3 第一步:安装alipay v3版本的安装依赖包; composer require alipaysdk/openapi:dev第二步:根据官方文档,把支付相关的类引用进来; <?php declare (strict_types 1);namespace app\p…...

高速信号的眼图、加重、均衡
目录 高速信号的眼图、加重、均衡眼图加重均衡线性均衡器CTLE判决反馈均衡器DFE 高速信号的眼图、加重、均衡 眼图 通常用示波器观察接收信号波形的眼图来分析码间串扰和噪声对系统性能的影响,从而估计系统优劣程度,因而眼图分析是高速互连系统信号完整…...

2024年PMP考前冲刺必背的学习笔记,整理好给你!
项目的四大特点:临时性、独特性、变革驱动性和创造商业价值。 项目管理:将知识、技能、工具与技术应用于项目活动,以满足项目的要求 Pestle:P政治,E经济,S社会,T技术,L法律,E环境 …...

增加服务器带宽可以提高资源加载速度吗?
答案是可以的 ,增加服务器带宽通常能够提高资源加速速度。带宽是服务器与互联网之间传输数据的速率,它决定了在单位时间内可以传输的数据量。以下是增加带宽如何提高资源加速速度的几个方面: 1.更快的数据传输:带宽增加后…...