Spring Boot 4.0 + MyBatis-Plus 实战响应式编程的能力实战
Spring Cloud全栈实战:手撸企业级项目,从入门到架构师!
目前 MyBatis-Plus 官方还不完全支持响应式编程,但我们可以结合 R2DBC 和 MyBatis-Plus 的部分特性来实现。这里提供两种方案:
方案一:使用 MyBatis-Plus 增强 R2DBC (推荐)
Spring Cloud全栈实战:手撸企业级项目,从入门到架构师!
1. 项目依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
<!-- 注意:只使用其工具类,不启用SQL执行 -->
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>io.asyncer</groupId>
<artifactId>r2dbc-mysql</artifactId>
<version>1.0.2</version>
</dependency>
<!-- 或 PostgreSQL -->
<!--
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
-->
2. 配置类
java
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.config.EnableR2dbcAuditing;
@Configuration
@EnableR2dbcAuditing
public class R2dbcMybatisConfig {
/**
* 只使用 MyBatis-Plus 的分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
3. 实体类 (使用 MyBatis-Plus 注解)
Spring Cloud全栈实战:手撸企业级项目,从入门到架构师!
java
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@Data
@Table("users")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("username")
private String username;
@TableField("email")
private String email;
@TableField("password")
private String password;
@TableField("age")
private Integer age;
@TableLogic
@TableField("deleted")
private Integer deleted = 0;
@TableField(value = "version", fill = FieldFill.INSERT)
@Version
private Integer version = 1;
@CreatedDate
@TableField("create_time")
private LocalDateTime createTime;
@LastModifiedDate
@TableField("update_time")
private LocalDateTime updateTime;
// 响应式编程友好的构造方法
public static Mono<User> of(String username, String email) {
User user = new User();
user.setUsername(username);
user.setEmail(email);
return Mono.just(user);
}
}
4. Repository 接口 (R2DBC)
java
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface UserR2dbcRepository extends R2dbcRepository<User, Long> {
Mono<User> findByUsername(String username);
Mono<User> findByEmail(String email);
Flux<User> findByAgeGreaterThan(Integer age);
@Query("SELECT * FROM users WHERE username LIKE :keyword OR email LIKE :keyword")
Flux<User> searchUsers(String keyword);
@Query("UPDATE users SET age = :age WHERE id = :id")
Mono<Integer> updateAgeById(Long id, Integer age);
}
5. Service 层 (结合 MyBatis-Plus 工具)
java
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.time.Duration;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class ReactiveUserService {
private final UserR2dbcRepository userRepository;
private final DatabaseClient databaseClient;
public Mono<User> createUser(User user) {
return userRepository.save(user);
}
public Mono<User> getUserById(Long id) {
return userRepository.findById(id)
.switchIfEmpty(Mono.error(new RuntimeException("User not found")));
}
public Flux<User> getAllUsers() {
return userRepository.findAll()
.delayElements(Duration.ofMillis(100)) // 模拟流处理
.subscribeOn(Schedulers.boundedElastic());
}
public Mono<User> updateUser(Long id, User user) {
return userRepository.findById(id)
.flatMap(existing -> {
existing.setUsername(user.getUsername());
existing.setEmail(user.getEmail());
existing.setAge(user.getAge());
return userRepository.save(existing);
});
}
@Transactional
public Mono<Void> deleteUser(Long id) {
return userRepository.deleteById(id)
.then(Mono.fromRunnable(() ->
System.out.println("User deleted: " + id)));
}
/**
* 使用 MyBatis-Plus 的 QueryWrapper 构建查询条件
* 然后转换为 R2DBC 查询
*/
public Flux<User> queryUsers(Map<String, Object> params) {
// 使用 MyBatis-Plus 的 QueryWrapper 构建条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (params.containsKey("username")) {
queryWrapper.like("username", params.get("username"));
}
if (params.containsKey("email")) {
queryWrapper.like("email", params.get("email"));
}
if (params.containsKey("minAge")) {
queryWrapper.ge("age", params.get("minAge"));
}
if (params.containsKey("maxAge")) {
queryWrapper.le("age", params.get("maxAge"));
}
queryWrapper.orderByDesc("create_time");
// 将 QueryWrapper 转换为 SQL
String sql = buildQueryWrapperSql(queryWrapper);
// 执行响应式查询
return databaseClient.sql(sql)
.fetch()
.all()
.map(row -> {
User user = new User();
user.setId((Long) row.get("id"));
user.setUsername((String) row.get("username"));
user.setEmail((String) row.get("email"));
user.setAge((Integer) row.get("age"));
return user;
});
}
/**
* 响应式分页查询
*/
public Mono<Page<User>> getUsersPage(Pageable pageable) {
// 使用 MyBatis-Plus 的 Page 对象
Page<User> mybatisPage = new Page<>(
pageable.getPageNumber(),
pageable.getPageSize()
);
// 计算总数
Mono<Long> countMono = databaseClient.sql("SELECT COUNT(*) FROM users")
.map(row -> row.get(0, Long.class))
.one();
// 查询数据
Flux<User> usersFlux = databaseClient.sql(
"SELECT * FROM users ORDER BY create_time DESC LIMIT :limit OFFSET :offset")
.bind("limit", pageable.getPageSize())
.bind("offset", pageable.getOffset())
.fetch()
.all()
.map(this::mapRowToUser);
return Mono.zip(countMono, usersFlux.collectList())
.map(tuple -> {
mybatisPage.setTotal(tuple.getT1());
mybatisPage.setRecords(tuple.getT2());
return mybatisPage;
});
}
private String buildQueryWrapperSql(QueryWrapper<User> queryWrapper) {
// 简化示例,实际需要更复杂的转换
return "SELECT * FROM users WHERE " + queryWrapper.getTargetSql();
}
private User mapRowToUser(Map<String, Object> row) {
User user = new User();
user.setId((Long) row.get("id"));
user.setUsername((String) row.get("username"));
user.setEmail((String) row.get("email"));
user.setAge((Integer) row.get("age"));
return user;
}
/**
* 批量保存
*/
public Flux<User> saveAll(Flux<User> users) {
return userRepository.saveAll(users)
.onErrorContinue((error, user) ->
System.err.println("Error saving user: " + error.getMessage()));
}
/**
* 流式查询
*/
public Flux<User> streamUsers() {
return databaseClient.sql("SELECT * FROM users")
.fetch()
.all()
.delayElements(Duration.ofMillis(50)) // 控制流速度
.map(this::mapRowToUser);
}
}
6. Controller 层
Spring Cloud全栈实战:手撸企业级项目,从入门到架构师!
java
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
import java.time.Duration;
import java.util.Map;
@RestController
@RequestMapping("/api/reactive/users")
@RequiredArgsConstructor
public class ReactiveUserController {
private final ReactiveUserService userService;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseEntity<User>> create(@Valid @RequestBody User user) {
return userService.createUser(user)
.map(saved -> ResponseEntity.status(HttpStatus.CREATED).body(saved))
.onErrorResume(e ->
Mono.just(ResponseEntity.badRequest().build()));
}
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getById(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@GetMapping
public Flux<User> getAll() {
return userService.getAllUsers();
}
@GetMapping("/search")
public Flux<User> search(@RequestParam Map<String, Object> params) {
return userService.queryUsers(params);
}
@GetMapping("/page")
public Mono<Page<User>> getPage(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
PageRequest pageRequest = PageRequest.of(page, size);
return userService.getUsersPage(pageRequest);
}
@PutMapping("/{id}")
public Mono<ResponseEntity<User>> update(
@PathVariable Long id,
@Valid @RequestBody User user) {
return userService.updateUser(id, user)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> delete(@PathVariable Long id) {
return userService.deleteUser(id)
.then(Mono.just(ResponseEntity.noContent().<Void>build()))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
/**
* Server-Sent Events (SSE) 流式接口
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<User> stream() {
return userService.streamUsers();
}
/**
* WebFlux WebSocket 支持
*/
@MessageMapping("users.chat")
public Flux<UserMessage> userChat(Flux<UserMessage> messages) {
return messages
.doOnNext(message ->
System.out.println("Received: " + message.getContent()))
.map(message -> new UserMessage("Server: " + message.getContent()))
.delayElements(Duration.ofSeconds(1));
}
/**
* 批量操作
*/
@PostMapping("/batch")
public Flux<User> batchCreate(@RequestBody Flux<User> users) {
return userService.saveAll(users);
}
}
7. 自定义响应式 Repository
Spring Cloud全栈实战:手撸企业级项目,从入门到架构师!
java
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface CustomReactiveRepository {
/**
* 使用 MyBatis-Plus 的 Lambda 查询
*/
Flux<User> findUsersByCondition(LambdaQueryWrapper<User> wrapper);
/**
* 响应式分页查询
*/
Mono<Page<User>> findPage(Page<User> page, LambdaQueryWrapper<User> wrapper);
}
8. 响应式事务配置
java
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.reactive.TransactionalOperator;
@Configuration
public class ReactiveTransactionConfig {
@Bean
public TransactionalOperator transactionalOperator(
ReactiveTransactionManager transactionManager) {
return TransactionalOperator.create(transactionManager);
}
}
方案二:使用 MyBatis-Plus 响应式扩展 (第三方)
有一些第三方项目正在尝试为 MyBatis-Plus 添加响应式支持:
Spring Cloud全栈实战:手撸企业级项目,从入门到架构师!
1. 添加依赖
xml
<!-- 第三方响应式扩展 -->
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join</artifactId>
<version>1.4.6</version>
</dependency>
2. 自定义响应式 Mapper
java
import org.apache.ibatis.annotations.SelectProvider;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ReactiveBaseMapper<T> {
Mono<Integer> insertReactive(T entity);
Mono<Integer> updateByIdReactive(T entity);
Mono<T> selectByIdReactive(Serializable id);
Flux<T> selectListReactive(Wrapper<T> queryWrapper);
Mono<Integer> deleteByIdReactive(Serializable id);
}
重要提示
- MyBatis-Plus 官方还不完全支持响应式,上述方案是结合 R2DBC 和 MyBatis-Plus 的工具类
- 真正的响应式编程需要使用 R2DBC 或 MongoDB Reactive
- 如果需要复杂 SQL 查询,可以使用 DatabaseClient 或 R2DBC Entity Callbacks
- 生产环境建议使用成熟的响应式数据库驱动
完整配置 application.yml
Spring Cloud全栈实战:手撸企业级项目,从入门到架构师!
yaml
spring:
r2dbc:
url: r2dbc:mysql://localhost:3306/reactive_db
username: root
password: password
pool:
initial-size: 5
max-size: 20
max-idle-time: 30m
webflux:
base-path: /api
static-path-pattern: /static/**
codec:
max-in-memory-size: 10MB
logging:
level:
org.springframework.r2dbc: DEBUG
io.r2dbc: DEBUG
这种架构结合了 MyBatis-Plus 的便利性和 R2DBC 的响应式能力,适合需要复杂查询的场景。