Spring Boot 集成 MongoDB
Spring Boot 集成 MongoDB 使用文档
一、环境准备与项目初始化
1.1 创建 Spring Boot 项目
使用 Spring Initializr 创建项目,选择以下依赖:
- Spring Web
- Spring Data MongoDB
- Lombok (可选但推荐)
- Validation
或使用 Maven 依赖配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>mongodb-demo</artifactId>
<version>1.0.0</version>
<name>mongodb-demo</name>
<properties>
<java.version>11</java.version>
<mongo.driver.version>4.7.2</mongo.driver.version>
</properties>
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data MongoDB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- MongoDB Reactive Support (可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- DevTools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
1.2 配置文件
application.yml 配置:
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: mongodb-demo
# MongoDB 配置
data:
mongodb:
# 单机配置
host: localhost
port: 27017
database: mydatabase
username: ${MONGO_USERNAME:admin}
password: ${MONGO_PASSWORD:admin123}
authentication-database: admin
# 连接池配置
auto-index-creation: true
# 副本集配置(可选)
# replica-set: rs0
# uri: mongodb://user:pass@host1:27017,host2:27017,host3:27017/database?replicaSet=rs0
# SSL 配置(可选)
# ssl:
# enabled: false
# invalid-hostname-allowed: false
# 自定义配置
mongodb:
connection:
max-pool-size: 100
min-pool-size: 10
max-wait-time: 120000
connect-timeout: 10000
socket-timeout: 0 # 0表示永不超时
application.properties 配置:
# MongoDB 配置
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=mydatabase
spring.data.mongodb.username=admin
spring.data.mongodb.password=admin123
spring.data.mongodb.authentication-database=admin
# 连接选项
spring.data.mongodb.auto-index-creation=true
# 连接池配置
spring.data.mongodb.uri=mongodb://admin:admin123@localhost:27017/mydatabase?authSource=admin
二、实体类设计
2.1 基本实体类
package com.example.mongodbdemo.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户实体类
* @Document - 指定MongoDB集合名称
* @CompoundIndex - 复合索引
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "users") // 指定集合名称,默认为类名小写
@CompoundIndexes({
@CompoundIndex(name = "email_status_idx", def = "{'email': 1, 'status': 1}"),
@CompoundIndex(name = "name_city_idx", def = "{'lastName': 1, 'city': 1}")
})
public class User {
@Id // 主键标识
private String id;
@NotBlank(message = "First name is required")
@Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
@Field("first_name") // 指定字段名,默认使用驼峰转下划线
private String firstName;
@NotBlank(message = "Last name is required")
@Size(min = 2, max = 50)
@Field("last_name")
private String lastName;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
@Indexed(unique = true, background = true) // 唯一索引,后台创建
private String email;
@NotNull(message = "Age is required")
private Integer age;
@Field("phone_number")
private String phoneNumber;
private String gender;
@Field("birth_date")
private LocalDateTime birthDate;
@Field("registration_date")
private LocalDateTime registrationDate;
private String city;
private String country;
@Indexed // 单字段索引
private String status; // ACTIVE, INACTIVE, DELETED
private List<String> roles; // 数组类型
@Field("preferences")
private Preferences preferences; // 嵌套文档
@Field("addresses")
private List<Address> addresses; // 嵌套文档数组
// 审计字段
@Field("created_at")
private LocalDateTime createdAt;
@Field("updated_at")
private LocalDateTime updatedAt;
@Field("created_by")
private String createdBy;
@Field("updated_by")
private String updatedBy;
// 版本控制(乐观锁)
@Version
private Long version;
/**
* 内嵌文档:用户偏好设置
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Preferences {
@Field("theme")
private String theme; // light, dark
@Field("language")
private String language;
@Field("notification_enabled")
private Boolean notificationEnabled;
@Field("email_notifications")
private Boolean emailNotifications;
@Field("sms_notifications")
private Boolean smsNotifications;
}
/**
* 内嵌文档:地址信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Address {
@Field("type")
private String type; // HOME, WORK
@Field("street")
private String street;
@Field("city")
private String city;
@Field("state")
private String state;
@Field("postal_code")
private String postalCode;
@Field("country")
private String country;
@Field("is_default")
private Boolean isDefault;
}
}
2.2 产品实体类(展示更多MongoDB特性)
package com.example.mongodbdemo.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 产品实体类(展示继承、多态)
*/
@Data
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
@Document(collection = "products")
public class Product extends BaseEntity {
@NotBlank(message = "Product name is required")
@Field("product_name")
private String productName;
@Field("sku")
@NotBlank(message = "SKU is required")
private String sku;
@Field("description")
private String description;
@NotNull(message = "Price is required")
@DecimalMin(value = "0.0", inclusive = false, message = "Price must be greater than 0")
@Field("price")
private BigDecimal price;
@Field("cost_price")
private BigDecimal costPrice;
@Field("quantity")
private Integer quantity;
@Field("category")
private String category;
@Field("tags")
private List<String> tags;
@Field("attributes")
private Map<String, Object> attributes; // 动态字段
@Field("specifications")
private List<Specification> specifications;
@Field("variants")
private List<ProductVariant> variants;
@Field("rating")
private Double rating;
@Field("review_count")
private Integer reviewCount;
@Field("images")
private List<Image> images;
@Field("is_active")
private Boolean isActive;
@Field("is_featured")
private Boolean isFeatured;
@Field("meta_data")
private MetaData metaData;
/**
* 规格
*/
@Data
@SuperBuilder
public static class Specification {
@Field("key")
private String key;
@Field("value")
private String value;
@Field("unit")
private String unit;
}
/**
* 产品变体
*/
@Data
@SuperBuilder
public static class ProductVariant {
@Field("variant_id")
private String variantId;
@Field("variant_name")
private String variantName;
@Field("price")
private BigDecimal price;
@Field("sku")
private String sku;
@Field("quantity")
private Integer quantity;
@Field("attributes")
private Map<String, String> attributes;
}
/**
* 图片
*/
@Data
@SuperBuilder
public static class Image {
@Field("url")
private String url;
@Field("alt_text")
private String altText;
@Field("is_primary")
private Boolean isPrimary;
@Field("order")
private Integer order;
}
/**
* 元数据
*/
@Data
@SuperBuilder
public static class MetaData {
@Field("seo_title")
private String seoTitle;
@Field("seo_description")
private String seoDescription;
@Field("keywords")
private List<String> keywords;
@Field("og_image")
private String ogImage;
}
}
/**
* 基础实体类(抽象类)
*/
@Data
@SuperBuilder
abstract class BaseEntity {
@org.springframework.data.annotation.Id
private String id;
@Field("created_at")
private java.time.LocalDateTime createdAt;
@Field("updated_at")
private java.time.LocalDateTime updatedAt;
@Field("created_by")
private String createdBy;
@Field("updated_by")
private String updatedBy;
@Field("is_deleted")
private Boolean isDeleted;
}
三、Repository 层
3.1 基础 Repository
package com.example.mongodbdemo.repository;
import com.example.mongodbdemo.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* 用户Repository
* 继承 MongoRepository 获得基本的CRUD操作
*/
@Repository
public interface UserRepository extends MongoRepository<User, String> {
// ========== 查询方法(根据方法名自动生成查询) ==========
// 等值查询
Optional<User> findByEmail(String email);
List<User> findByFirstName(String firstName);
List<User> findByLastName(String lastName);
// 条件查询
List<User> findByAgeGreaterThan(Integer age);
List<User> findByAgeLessThan(Integer age);
List<User> findByAgeBetween(Integer start, Integer end);
List<User> findByAgeIn(List<Integer> ages);
// 多条件查询
List<User> findByFirstNameAndLastName(String firstName, String lastName);
List<User> findByFirstNameOrLastName(String firstName, String lastName);
// 模糊查询
List<User> findByFirstNameLike(String pattern);
List<User> findByFirstNameContaining(String keyword);
List<User> findByFirstNameStartingWith(String prefix);
List<User> findByFirstNameEndingWith(String suffix);
// 忽略大小写
List<User> findByFirstNameIgnoreCase(String firstName);
// 嵌套文档查询
List<User> findByPreferencesTheme(String theme);
List<User> findByPreferencesNotificationEnabledTrue();
// 数组查询
List<User> findByRolesContaining(String role);
// 排序和分页
List<User> findByCityOrderByAgeDesc(String city);
Page<User> findByStatus(String status, Pageable pageable);
// 计数
Long countByCity(String city);
Long countByStatus(String status);
// 删除
void deleteByEmail(String email);
Long deleteByStatus(String status);
// 是否存在
Boolean existsByEmail(String email);
// ========== 自定义查询(使用 @Query 注解) ==========
/**
* 使用原生MongoDB查询语法
* ?0, ?1 表示方法参数位置
*/
@Query("{ 'age' : { $gt: ?0, $lt: ?1 } }")
List<User> findUsersByAgeRange(Integer minAge, Integer maxAge);
/**
* 使用 SpEL 表达式
*/
@Query("{ 'email' : :#{#email} }")
Optional<User> findUserByEmail(@Param("email") String email);
/**
* 复杂查询:多条件 + 排序 + 字段投影
*/
@Query(value = "{ 'city': ?0, 'status': 'ACTIVE', 'age': { $gte: ?1 } }",
sort = "{ 'registration_date': -1 }",
fields = "{ 'firstName': 1, 'lastName': 1, 'email': 1, 'age': 1 }")
List<User> findActiveUsersInCity(String city, Integer minAge);
/**
* 正则表达式查询
*/
@Query("{ 'email': { $regex: ?0, $options: 'i' } }")
List<User> findUsersByEmailPattern(String pattern);
/**
* 数组查询
*/
@Query("{ 'roles': { $all: ?0 } }")
List<User> findUsersWithAllRoles(List<String> roles);
/**
* 数组大小查询
*/
@Query("{ 'roles': { $size: ?0 } }")
List<User> findUsersWithRoleCount(Integer count);
/**
* 日期范围查询
*/
@Query("{ 'registration_date': { $gte: ?0, $lte: ?1 } }")
List<User> findUsersRegisteredBetween(LocalDateTime start, LocalDateTime end);
/**
* 嵌套文档查询
*/
@Query("{ 'addresses.city': ?0, 'addresses.is_default': true }")
List<User> findUsersWithDefaultAddressInCity(String city);
/**
* 聚合查询:分组统计
*/
@Query(value = "{}", fields = "{ 'city': 1 }")
List<User> findAllCities();
// ========== 自定义查询方法(使用 @Aggregation 注解) ==========
/**
* 聚合查询:按城市分组统计用户数
*/
@Aggregation(pipeline = {
"{ $match: { status: 'ACTIVE' } }",
"{ $group: { _id: '$city', count: { $sum: 1 }, avgAge: { $avg: '$age' } } }",
"{ $sort: { count: -1 } }",
"{ $limit: 10 }"
})
List<CityStats> countUsersByCity();
/**
* 聚合结果映射类
*/
interface CityStats {
String get_id(); // MongoDB分组后的_id字段
Long getCount();
Double getAvgAge();
}
/**
* 聚合查询:用户年龄分布
*/
@Aggregation(pipeline = {
"{ $bucket: { " +
"groupBy: '$age', " +
"boundaries: [18, 25, 35, 45, 55, 65], " +
"default: '65+', " +
"output: { " +
"count: { $sum: 1 }, " +
"users: { $push: { name: { $concat: ['$firstName', ' ', '$lastName'] }, email: '$email' } } " +
"}" +
"}}"
})
List<AgeDistribution> getUserAgeDistribution();
interface AgeDistribution {
String get_id();
Long getCount();
List<UserInfo> getUsers();
interface UserInfo {
String getName();
String getEmail();
}
}
}
3.2 自定义 Repository 实现
package com.example.mongodbdemo.repository.impl;
import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.custom.UserCustomRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 自定义Repository实现
*/
@Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserCustomRepository {
private final MongoTemplate mongoTemplate;
@Override
public Page<User> searchUsers(Map<String, Object> criteria, Pageable pageable) {
Query query = new Query();
// 动态构建查询条件
if (criteria.containsKey("firstName")) {
query.addCriteria(Criteria.where("first_name").regex(criteria.get("firstName").toString(), "i"));
}
if (criteria.containsKey("lastName")) {
query.addCriteria(Criteria.where("last_name").regex(criteria.get("lastName").toString(), "i"));
}
if (criteria.containsKey("email")) {
query.addCriteria(Criteria.where("email").regex(criteria.get("email").toString(), "i"));
}
if (criteria.containsKey("ageFrom") && criteria.containsKey("ageTo")) {
query.addCriteria(Criteria.where("age")
.gte(Integer.parseInt(criteria.get("ageFrom").toString()))
.lte(Integer.parseInt(criteria.get("ageTo").toString())));
}
if (criteria.containsKey("city")) {
query.addCriteria(Criteria.where("city").is(criteria.get("city")));
}
if (criteria.containsKey("status")) {
query.addCriteria(Criteria.where("status").is(criteria.get("status")));
}
// 分页和排序
query.with(pageable);
// 获取总数
long total = mongoTemplate.count(query, User.class);
// 获取数据
List<User> users = mongoTemplate.find(query, User.class);
return new PageImpl<>(users, pageable, total);
}
@Override
public void updateUserStatus(String userId, String status) {
Query query = new Query(Criteria.where("id").is(userId));
Update update = new Update()
.set("status", status)
.set("updated_at", LocalDateTime.now());
mongoTemplate.updateFirst(query, update, User.class);
}
@Override
public void updateUserEmail(String userId, String newEmail) {
Query query = new Query(Criteria.where("id").is(userId));
Update update = new Update()
.set("email", newEmail)
.set("updated_at", LocalDateTime.now());
mongoTemplate.updateFirst(query, update, User.class);
}
@Override
public void bulkUpdateUserStatus(List<String> userIds, String status) {
Query query = new Query(Criteria.where("id").in(userIds));
Update update = new Update()
.set("status", status)
.set("updated_at", LocalDateTime.now());
mongoTemplate.updateMulti(query, update, User.class);
}
@Override
public List<Map> getUserStatistics() {
// 使用聚合框架进行复杂统计
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("status").is("ACTIVE")),
Aggregation.group("city")
.count().as("userCount")
.avg("age").as("avgAge")
.sum("age").as("totalAge"),
Aggregation.project("userCount", "avgAge", "totalAge")
.and("_id").as("city"),
Aggregation.sort(Sort.Direction.DESC, "userCount")
);
AggregationResults<Map> results = mongoTemplate
.aggregate(aggregation, "users", Map.class);
return results.getMappedResults();
}
@Override
public List<User> findUsersByCustomQuery(String firstNamePattern, Integer minAge, String city) {
Criteria criteria = new Criteria();
List<Criteria> criteriaList = new java.util.ArrayList<>();
if (firstNamePattern != null && !firstNamePattern.isEmpty()) {
criteriaList.add(Criteria.where("first_name").regex(firstNamePattern, "i"));
}
if (minAge != null) {
criteriaList.add(Criteria.where("age").gte(minAge));
}
if (city != null && !city.isEmpty()) {
criteriaList.add(Criteria.where("city").is(city));
}
if (!criteriaList.isEmpty()) {
criteria.andOperator(criteriaList.toArray(new Criteria[0]));
}
Query query = new Query(criteria);
query.with(Sort.by(Sort.Direction.DESC, "registration_date"));
return mongoTemplate.find(query, User.class);
}
@Override
public List<String> findAllDistinctCities() {
return mongoTemplate.findDistinct(
new Query(), "city", User.class, String.class
);
}
@Override
public Long countUsersByCriteria(Map<String, Object> criteria) {
Query query = new Query();
criteria.forEach((key, value) -> {
if (value != null) {
switch (key) {
case "ageFrom":
query.addCriteria(Criteria.where("age").gte(value));
break;
case "ageTo":
query.addCriteria(Criteria.where("age").lte(value));
break;
case "city":
query.addCriteria(Criteria.where("city").is(value));
break;
case "status":
query.addCriteria(Criteria.where("status").is(value));
break;
}
}
});
return mongoTemplate.count(query, User.class);
}
}
/**
* 自定义Repository接口
*/
package com.example.mongodbdemo.repository.custom;
import com.example.mongodbdemo.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Map;
public interface UserCustomRepository {
Page<User> searchUsers(Map<String, Object> criteria, Pageable pageable);
void updateUserStatus(String userId, String status);
void updateUserEmail(String userId, String newEmail);
void bulkUpdateUserStatus(List<String> userIds, String status);
List<Map> getUserStatistics();
List<User> findUsersByCustomQuery(String firstNamePattern, Integer minAge, String city);
List<String> findAllDistinctCities();
Long countUsersByCriteria(Map<String, Object> criteria);
}
3.3 扩展 Repository 接口
package com.example.mongodbdemo.repository;
import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.custom.UserCustomRepository;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends MongoRepository<User, String>, UserCustomRepository {
// 继承自定义接口
}
四、Service 层
4.1 基础 Service
package com.example.mongodbdemo.service;
import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* 用户服务层
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
/**
* 创建用户
*/
@Transactional
public User createUser(User user) {
// 验证邮箱是否已存在
if (userRepository.existsByEmail(user.getEmail())) {
throw new RuntimeException("Email already exists: " + user.getEmail());
}
// 设置审计字段
user.setId(null); // 确保ID由MongoDB生成
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
user.setStatus("ACTIVE");
// 保存用户
User savedUser = userRepository.save(user);
log.info("Created user with ID: {}", savedUser.getId());
return savedUser;
}
/**
* 批量创建用户
*/
@Transactional
public List<User> createUsers(List<User> users) {
// 验证邮箱唯一性
List<String> emails = users.stream()
.map(User::getEmail)
.toList();
List<User> existingUsers = userRepository.findByEmailIn(emails);
if (!existingUsers.isEmpty()) {
throw new RuntimeException("Some emails already exist");
}
// 设置审计字段
LocalDateTime now = LocalDateTime.now();
users.forEach(user -> {
user.setId(null);
user.setCreatedAt(now);
user.setUpdatedAt(now);
user.setStatus("ACTIVE");
});
return userRepository.saveAll(users);
}
/**
* 根据ID获取用户
*/
public Optional<User> getUserById(String id) {
return userRepository.findById(id);
}
/**
* 根据邮箱获取用户
*/
public Optional<User> getUserByEmail(String email) {
return userRepository.findByEmail(email);
}
/**
* 获取所有用户
*/
public List<User> getAllUsers() {
return userRepository.findAll();
}
/**
* 分页获取用户
*/
public Page<User> getUsers(Pageable pageable) {
return userRepository.findAll(pageable);
}
/**
* 更新用户
*/
@Transactional
public User updateUser(String id, User userUpdates) {
User existingUser = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
// 更新允许修改的字段
if (StringUtils.hasText(userUpdates.getFirstName())) {
existingUser.setFirstName(userUpdates.getFirstName());
}
if (StringUtils.hasText(userUpdates.getLastName())) {
existingUser.setLastName(userUpdates.getLastName());
}
if (userUpdates.getAge() != null) {
existingUser.setAge(userUpdates.getAge());
}
if (StringUtils.hasText(userUpdates.getPhoneNumber())) {
existingUser.setPhoneNumber(userUpdates.getPhoneNumber());
}
if (StringUtils.hasText(userUpdates.getCity())) {
existingUser.setCity(userUpdates.getCity());
}
if (StringUtils.hasText(userUpdates.getCountry())) {
existingUser.setCountry(userUpdates.getCountry());
}
// 更新审计字段
existingUser.setUpdatedAt(LocalDateTime.now());
return userRepository.save(existingUser);
}
/**
* 部分更新用户
*/
@Transactional
public void partialUpdateUser(String id, Map<String, Object> updates) {
User existingUser = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
updates.forEach((key, value) -> {
switch (key) {
case "firstName":
existingUser.setFirstName((String) value);
break;
case "lastName":
existingUser.setLastName((String) value);
break;
case "age":
existingUser.setAge((Integer) value);
break;
case "phoneNumber":
existingUser.setPhoneNumber((String) value);
break;
case "city":
existingUser.setCity((String) value);
break;
case "country":
existingUser.setCountry((String) value);
break;
case "status":
existingUser.setStatus((String) value);
break;
}
});
existingUser.setUpdatedAt(LocalDateTime.now());
userRepository.save(existingUser);
}
/**
* 删除用户
*/
@Transactional
public void deleteUser(String id) {
if (!userRepository.existsById(id)) {
throw new RuntimeException("User not found with id: " + id);
}
userRepository.deleteById(id);
log.info("Deleted user with ID: {}", id);
}
/**
* 软删除用户
*/
@Transactional
public void softDeleteUser(String id) {
userRepository.updateUserStatus(id, "DELETED");
}
/**
* 搜索用户
*/
public Page<User> searchUsers(Map<String, Object> criteria, Pageable pageable) {
return userRepository.searchUsers(criteria, pageable);
}
/**
* 根据条件查询用户
*/
public List<User> findUsersByCriteria(String firstNamePattern, Integer minAge, String city) {
return userRepository.findUsersByCustomQuery(firstNamePattern, minAge, city);
}
/**
* 获取用户统计
*/
public List<Map> getUserStatistics() {
return userRepository.getUserStatistics();
}
/**
* 获取所有城市
*/
public List<String> getAllCities() {
return userRepository.findAllDistinctCities();
}
/**
* 更新用户状态
*/
@Transactional
public void updateUserStatus(String id, String status) {
if (!List.of("ACTIVE", "INACTIVE", "SUSPENDED", "DELETED").contains(status)) {
throw new RuntimeException("Invalid status: " + status);
}
userRepository.updateUserStatus(id, status);
}
/**
* 批量更新用户状态
*/
@Transactional
public void bulkUpdateUserStatus(List<String> ids, String status) {
if (!List.of("ACTIVE", "INACTIVE", "SUSPENDED", "DELETED").contains(status)) {
throw new RuntimeException("Invalid status: " + status);
}
userRepository.bulkUpdateUserStatus(ids, status);
}
/**
* 根据条件统计用户数
*/
public Long countUsers(Map<String, Object> criteria) {
return userRepository.countUsersByCriteria(criteria);
}
/**
* 验证用户是否存在
*/
public boolean userExists(String id) {
return userRepository.existsById(id);
}
/**
* 验证邮箱是否已存在
*/
public boolean emailExists(String email) {
return userRepository.existsByEmail(email);
}
}
4.2 高级 Service(包含事务和异常处理)
package com.example.mongodbdemo.service;
import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.exception.ResourceNotFoundException;
import com.example.mongodbdemo.exception.ValidationException;
import com.example.mongodbdemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* 高级用户服务(包含重试机制、事务管理等)
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AdvancedUserService {
private final UserRepository userRepository;
private final MongoTemplate mongoTemplate;
/**
* 创建用户(带重试机制)
*/
@Retryable(
value = {DuplicateKeyException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
@Transactional
public User createUserWithRetry(User user) {
try {
user.setId(null);
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
return userRepository.save(user);
} catch (DuplicateKeyException e) {
log.error("Duplicate key error when creating user", e);
throw new ValidationException("Email already exists");
}
}
/**
* 使用 MongoTemplate 执行复杂操作
*/
@Transactional
public void updateUserWithMongoTemplate(String userId, Map<String, Object> updates) {
Query query = new Query(Criteria.where("id").is(userId));
User existingUser = mongoTemplate.findOne(query, User.class);
if (existingUser == null) {
throw new ResourceNotFoundException("User not found");
}
Update update = new Update();
updates.forEach((key, value) -> {
switch (key) {
case "firstName":
update.set("first_name", value);
break;
case "lastName":
update.set("last_name", value);
break;
case "age":
update.set("age", value);
break;
case "email":
// 验证邮箱是否唯一
if (!existingUser.getEmail().equals(value)) {
checkEmailUniqueness((String) value);
update.set("email", value);
}
break;
}
});
update.set("updated_at", LocalDateTime.now());
mongoTemplate.updateFirst(query, update, User.class);
}
/**
* 使用事务执行多个操作
*/
@Transactional
public void executeInTransaction(List<Runnable> operations) {
operations.forEach(Runnable::run);
}
/**
* 批量操作
*/
@Transactional
public void batchInsert(List<User> users) {
users.forEach(user -> {
user.setId(null);
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
});
mongoTemplate.insertAll(users);
}
/**
* 使用聚合查询
*/
public Map<String, Object> getUserAnalytics() {
List<Map> cityStats = userRepository.getUserStatistics();
List<UserRepository.CityStats> ageStats = userRepository.countUsersByCity();
return Map.of(
"cityStatistics", cityStats,
"ageStatistics", ageStats,
"totalUsers", userRepository.count(),
"activeUsers", userRepository.countByStatus("ACTIVE")
);
}
/**
* 地理空间查询
*/
public List<User> findUsersNearLocation(double longitude, double latitude, double maxDistance) {
Query query = new Query(Criteria.where("location")
.nearSphere(new org.springframework.data.geo.Point(longitude, latitude))
.maxDistance(maxDistance));
return mongoTemplate.find(query, User.class);
}
/**
* 文本搜索
*/
public List<User> searchUsersByText(String searchText) {
Query query = new Query(Criteria.where("$text")
.matching(new org.springframework.data.mongodb.core.query.TextCriteria()
.matching(searchText)));
query.limit(50);
return mongoTemplate.find(query, User.class);
}
private void checkEmailUniqueness(String email) {
Query query = new Query(Criteria.where("email").is(email));
long count = mongoTemplate.count(query, User.class);
if (count > 0) {
throw new ValidationException("Email already exists");
}
}
}
五、Controller 层
5.1 RESTful API 控制器
package com.example.mongodbdemo.controller;
import com.example.mongodbdemo.dto.*;
import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.util.List;
import java.util.Map;
/**
* 用户管理API
*/
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Slf4j
@Validated
@Tag(name = "用户管理", description = "用户管理相关API")
public class UserController {
private final UserService userService;
/**
* 创建用户
*/
@PostMapping
@Operation(summary = "创建用户", description = "创建新用户")
public ResponseEntity<ApiResponse<User>> createUser(
@Valid @RequestBody CreateUserRequest request) {
log.info("Creating user with email: {}", request.getEmail());
User user = convertToEntity(request);
User createdUser = userService.createUser(user);
ApiResponse<User> response = ApiResponse.success(
createdUser,
"用户创建成功"
);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
/**
* 批量创建用户
*/
@PostMapping("/batch")
@Operation(summary = "批量创建用户")
public ResponseEntity<ApiResponse<List<User>>> createUsers(
@Valid @RequestBody List<CreateUserRequest> requests) {
log.info("Creating {} users", requests.size());
List<User> users = requests.stream()
.map(this::convertToEntity)
.toList();
List<User> createdUsers = userService.createUsers(users);
ApiResponse<List<User>> response = ApiResponse.success(
createdUsers,
String.format("成功创建 %d 个用户", createdUsers.size())
);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
/**
* 获取用户详情
*/
@GetMapping("/{id}")
@Operation(summary = "获取用户详情", description = "根据ID获取用户信息")
public ResponseEntity<ApiResponse<User>> getUserById(
@PathVariable @Parameter(description = "用户ID", required = true) String id) {
log.info("Getting user by ID: {}", id);
return userService.getUserById(id)
.map(user -> ResponseEntity.ok(ApiResponse.success(user, "获取成功")))
.orElse(ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("用户不存在")));
}
/**
* 根据邮箱获取用户
*/
@GetMapping("/email/{email}")
@Operation(summary = "根据邮箱获取用户")
public ResponseEntity<ApiResponse<User>> getUserByEmail(
@PathVariable String email) {
log.info("Getting user by email: {}", email);
return userService.getUserByEmail(email)
.map(user -> ResponseEntity.ok(ApiResponse.success(user, "获取成功")))
.orElse(ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("用户不存在")));
}
/**
* 获取所有用户
*/
@GetMapping
@Operation(summary = "获取用户列表", description = "获取所有用户,支持分页和排序")
public ResponseEntity<ApiResponse<Page<User>>> getAllUsers(
@PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
log.info("Getting all users with pageable: {}", pageable);
Page<User> users = userService.getUsers(pageable);
ApiResponse<Page<User>> response = ApiResponse.success(
users,
String.format("获取到 %d 个用户", users.getTotalElements())
);
return ResponseEntity.ok(response);
}
/**
* 搜索用户
*/
@GetMapping("/search")
@Operation(summary = "搜索用户", description = "根据条件搜索用户")
public ResponseEntity<ApiResponse<Page<User>>> searchUsers(
@RequestParam(required = false) String firstName,
@RequestParam(required = false) String lastName,
@RequestParam(required = false) String email,
@RequestParam(required = false) Integer ageFrom,
@RequestParam(required = false) Integer ageTo,
@RequestParam(required = false) String city,
@RequestParam(required = false) String status,
@PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
log.info("Searching users with criteria");
Map<String, Object> criteria = new java.util.HashMap<>();
if (firstName != null) criteria.put("firstName", firstName);
if (lastName != null) criteria.put("lastName", lastName);
if (email != null) criteria.put("email", email);
if (ageFrom != null) criteria.put("ageFrom", ageFrom);
if (ageTo != null) criteria.put("ageTo", ageTo);
if (city != null) criteria.put("city", city);
if (status != null) criteria.put("status", status);
Page<User> users = userService.searchUsers(criteria, pageable);
ApiResponse<Page<User>> response = ApiResponse.success(
users,
String.format("搜索到 %d 个用户", users.getTotalElements())
);
return ResponseEntity.ok(response);
}
/**
* 更新用户
*/
@PutMapping("/{id}")
@Operation(summary = "更新用户", description = "更新用户信息")
public ResponseEntity<ApiResponse<User>> updateUser(
@PathVariable String id,
@Valid @RequestBody UpdateUserRequest request) {
log.info("Updating user with ID: {}", id);
User userUpdates = convertToEntity(request);
User updatedUser = userService.updateUser(id, userUpdates);
ApiResponse<User> response = ApiResponse.success(
updatedUser,
"用户更新成功"
);
return ResponseEntity.ok(response);
}
/**
* 部分更新用户
*/
@PatchMapping("/{id}")
@Operation(summary = "部分更新用户", description = "更新用户部分字段")
public ResponseEntity<ApiResponse<Void>> partialUpdateUser(
@PathVariable String id,
@RequestBody Map<String, Object> updates) {
log.info("Partial updating user with ID: {}", id);
userService.partialUpdateUser(id, updates);
return ResponseEntity.ok(ApiResponse.success("用户更新成功"));
}
/**
* 删除用户
*/
@DeleteMapping("/{id}")
@Operation(summary = "删除用户", description = "根据ID删除用户")
public ResponseEntity<ApiResponse<Void>> deleteUser(
@PathVariable String id) {
log.info("Deleting user with ID: {}", id);
userService.deleteUser(id);
return ResponseEntity.ok(ApiResponse.success("用户删除成功"));
}
/**
* 软删除用户
*/
@DeleteMapping("/{id}/soft")
@Operation(summary = "软删除用户")
public ResponseEntity<ApiResponse<Void>> softDeleteUser(
@PathVariable String id) {
log.info("Soft deleting user with ID: {}", id);
userService.softDeleteUser(id);
return ResponseEntity.ok(ApiResponse.success("用户已标记为删除"));
}
/**
* 获取用户统计信息
*/
@GetMapping("/statistics")
@Operation(summary = "获取用户统计信息")
public ResponseEntity<ApiResponse<List<Map>>> getUserStatistics() {
log.info("Getting user statistics");
List<Map> statistics = userService.getUserStatistics();
ApiResponse<List<Map>> response = ApiResponse.success(
statistics,
"获取统计信息成功"
);
return ResponseEntity.ok(response);
}
/**
* 获取所有城市
*/
@GetMapping("/cities")
@Operation(summary = "获取所有城市")
public ResponseEntity<ApiResponse<List<String>>> getAllCities() {
log.info("Getting all cities");
List<String> cities = userService.getAllCities();
ApiResponse<List<String>> response = ApiResponse.success(
cities,
String.format("获取到 %d 个城市", cities.size())
);
return ResponseEntity.ok(response);
}
/**
* 验证邮箱是否可用
*/
@GetMapping("/check-email")
@Operation(summary = "验证邮箱是否可用")
public ResponseEntity<ApiResponse<Boolean>> checkEmailAvailability(
@RequestParam String email) {
log.info("Checking email availability: {}", email);
boolean available = !userService.emailExists(email);
String message = available ? "邮箱可用" : "邮箱已被使用";
return ResponseEntity.ok(ApiResponse.success(available, message));
}
/**
* 根据条件查询用户
*/
@GetMapping("/query")
@Operation(summary = "根据条件查询用户")
public ResponseEntity<ApiResponse<List<User>>> queryUsers(
@RequestParam(required = false) String firstName,
@RequestParam(required = false) @Min(0) Integer minAge,
@RequestParam(required = false) String city) {
log.info("Querying users with criteria");
List<User> users = userService.findUsersByCriteria(firstName, minAge, city);
ApiResponse<List<User>> response = ApiResponse.success(
users,
String.format("查询到 %d 个用户", users.size())
);
return ResponseEntity.ok(response);
}
/**
* 更新用户状态
*/
@PutMapping("/{id}/status")
@Operation(summary = "更新用户状态")
public ResponseEntity<ApiResponse<Void>> updateUserStatus(
@PathVariable String id,
@RequestParam String status) {
log.info("Updating user status, ID: {}, status: {}", id, status);
userService.updateUserStatus(id, status);
return ResponseEntity.ok(ApiResponse.success("用户状态更新成功"));
}
/**
* 批量更新用户状态
*/
@PutMapping("/batch-status")
@Operation(summary = "批量更新用户状态")
public ResponseEntity<ApiResponse<Void>> bulkUpdateUserStatus(
@RequestParam List<String> ids,
@RequestParam String status) {
log.info("Bulk updating user status for {} users", ids.size());
userService.bulkUpdateUserStatus(ids, status);
return ResponseEntity.ok(ApiResponse.success("批量更新成功"));
}
/**
* 根据条件统计用户数
*/
@GetMapping("/count")
@Operation(summary = "根据条件统计用户数")
public ResponseEntity<ApiResponse<Long>> countUsers(
@RequestParam(required = false) Integer ageFrom,
@RequestParam(required = false) Integer ageTo,
@RequestParam(required = false) String city,
@RequestParam(required = false) String status) {
log.info("Counting users with criteria");
Map<String, Object> criteria = new java.util.HashMap<>();
if (ageFrom != null) criteria.put("ageFrom", ageFrom);
if (ageTo != null) criteria.put("ageTo", ageTo);
if (city != null) criteria.put("city", city);
if (status != null) criteria.put("status", status);
Long count = userService.countUsers(criteria);
return ResponseEntity.ok(ApiResponse.success(count, "统计完成"));
}
/**
* 转换请求为实体
*/
private User convertToEntity(CreateUserRequest request) {
return User.builder()
.firstName(request.getFirstName())
.lastName(request.getLastName())
.email(request.getEmail())
.age(request.getAge())
.phoneNumber(request.getPhoneNumber())
.gender(request.getGender())
.birthDate(request.getBirthDate())
.city(request.getCity())
.country(request.getCountry())
.roles(request.getRoles())
.preferences(request.getPreferences())
.addresses(request.getAddresses())
.build();
}
private User convertToEntity(UpdateUserRequest request) {
return User.builder()
.firstName(request.getFirstName())
.lastName(request.getLastName())
.age(request.getAge())
.phoneNumber(request.getPhoneNumber())
.city(request.getCity())
.country(request.getCountry())
.build();
}
}
六、DTO 和请求/响应对象
6.1 请求对象
package com.example.mongodbdemo.dto;
import com.example.mongodbdemo.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.*;
import java.time.LocalDateTime;
import java.util.List;
/**
* 创建用户请求
*/
@Data
@Schema(description = "创建用户请求")
public class CreateUserRequest {
@NotBlank(message = "First name is required")
@Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
@Schema(description = "First name", example = "John", required = true)
private String firstName;
@NotBlank(message = "Last name is required")
@Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters")
@Schema(description = "Last name", example = "Doe", required = true)
private String lastName;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
@Schema(description = "Email address", example = "john.doe@example.com", required = true)
private String email;
@NotNull(message = "Age is required")
@Min(value = 0, message = "Age must be positive")
@Max(value = 150, message = "Age must be less than 150")
@Schema(description = "Age", example = "25", required = true)
private Integer age;
@Pattern(regexp = "^[0-9\\-+()\\s]*$", message = "Invalid phone number format")
@Schema(description = "Phone number", example = "+1-234-567-8900")
private String phoneNumber;
@Pattern(regexp = "^(MALE|FEMALE|OTHER)$", message = "Invalid gender")
@Schema(description = "Gender", example = "MALE")
private String gender;
@Schema(description = "Birth date")
private LocalDateTime birthDate;
@Schema(description = "City", example = "New York")
private String city;
@Schema(description = "Country", example = "USA")
private String country;
@Schema(description = "User roles")
private List<String> roles;
@Schema(description = "User preferences")
private User.Preferences preferences;
@Schema(description = "User addresses")
private List<User.Address> addresses;
}
/**
* 更新用户请求
*/
@Data
@Schema(description = "更新用户请求")
public class UpdateUserRequest {
@Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
@Schema(description = "First name", example = "John")
private String firstName;
@Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters")
@Schema(description = "Last name", example = "Doe")
private String lastName;
@Min(value = 0, message = "Age must be positive")
@Max(value = 150, message = "Age must be less than 150")
@Schema(description = "Age", example = "26")
private Integer age;
@Pattern(regexp = "^[0-9\\-+()\\s]*$", message = "Invalid phone number format")
@Schema(description = "Phone number", example = "+1-234-567-8901")
private String phoneNumber;
@Schema(description = "City", example = "Los Angeles")
private String city;
@Schema(description = "Country", example = "USA")
private String country;
}
/**
* 搜索用户请求
*/
@Data
@Schema(description = "搜索用户请求")
public class SearchUserRequest {
@Schema(description = "First name", example = "John")
private String firstName;
@Schema(description = "Last name", example = "Doe")
private String lastName;
@Email(message = "Invalid email format")
@Schema(description = "Email", example = "john@example.com")
private String email;
@Schema(description = "Minimum age", example = "18")
private Integer minAge;
@Schema(description = "Maximum age", example = "60")
private Integer maxAge;
@Schema(description = "City", example = "New York")
private String city;
@Schema(description = "Status", example = "ACTIVE")
private String status;
@Schema(description = "Page number", example = "0")
private Integer page;
@Schema(description = "Page size", example = "20")
private Integer size;
@Schema(description = "Sort field", example = "createdAt")
private String sort;
@Schema(description = "Sort direction", example = "DESC")
private String direction;
}
6.2 响应对象
package com.example.mongodbdemo.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 统一API响应
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "API响应")
public class ApiResponse<T> {
@Schema(description = "是否成功", example = "true")
private boolean success;
@Schema(description = "响应消息", example = "操作成功")
private String message;
@Schema(description = "响应数据")
private T data;
@Schema(description = "错误代码")
private String errorCode;
@Schema(description = "时间戳", example = "2024-01-15T10:30:00")
private LocalDateTime timestamp;
/**
* 成功响应
*/
public static <T> ApiResponse<T> success(T data, String message) {
return ApiResponse.<T>builder()
.success(true)
.message(message)
.data(data)
.timestamp(LocalDateTime.now())
.build();
}
public static <T> ApiResponse<T> success(T data) {
return success(data, "操作成功");
}
public static ApiResponse<Void> success(String message) {
return success(null, message);
}
/**
* 失败响应
*/
public static <T> ApiResponse<T> error(String message, String errorCode) {
return ApiResponse.<T>builder()
.success(false)
.message(message)
.errorCode(errorCode)
.timestamp(LocalDateTime.now())
.build();
}
public static <T> ApiResponse<T> error(String message) {
return error(message, null);
}
}
/**
* 用户响应DTO(展示如何控制返回字段)
*/
@Data
@Builder
@Schema(description = "用户响应")
public class UserResponse {
@Schema(description = "用户ID", example = "507f1f77bcf86cd799439011")
private String id;
@Schema(description = "First name", example = "John")
private String firstName;
@Schema(description = "Last name", example = "Doe")
private String lastName;
@Schema(description = "Email", example = "john.doe@example.com")
private String email;
@Schema(description = "Age", example = "25")
private Integer age;
@Schema(description = "City", example = "New York")
private String city;
@Schema(description = "Country", example = "USA")
private String country;
@Schema(description = "Status", example = "ACTIVE")
private String status;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
/**
* 从实体转换
*/
public static UserResponse fromEntity(com.example.mongodbdemo.entity.User user) {
return UserResponse.builder()
.id(user.getId())
.firstName(user.getFirstName())
.lastName(user.getLastName())
.email(user.getEmail())
.age(user.getAge())
.city(user.getCity())
.country(user.getCountry())
.status(user.getStatus())
.createdAt(user.getCreatedAt())
.build();
}
}
七、配置类
7.1 MongoDB 配置
package com.example.mongodbdemo.config;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Date;
/**
* MongoDB配置类
*/
@Configuration
@EnableMongoRepositories(basePackages = "com.example.mongodbdemo.repository")
@Slf4j
public class MongoConfig extends AbstractMongoClientConfiguration {
@Value("${spring.data.mongodb.host:localhost}")
private String host;
@Value("${spring.data.mongodb.port:27017}")
private int port;
@Value("${spring.data.mongodb.database:mydatabase}")
private String database;
@Value("${spring.data.mongodb.username:}")
private String username;
@Value("${spring.data.mongodb.password:}")
private String password;
@Value("${spring.data.mongodb.authentication-database:admin}")
private String authenticationDatabase;
@Override
protected String getDatabaseName() {
return database;
}
@Override
public MongoClient mongoClient() {
log.info("Connecting to MongoDB: {}:{}, database: {}", host, port, database);
String connectionString;
if (username.isEmpty() || password.isEmpty()) {
// 无认证连接
connectionString = String.format("mongodb://%s:%d/%s", host, port, database);
} else {
// 带认证连接
connectionString = String.format(
"mongodb://%s:%s@%s:%d/%s?authSource=%s",
username, password, host, port, database, authenticationDatabase
);
}
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(new ConnectionString(connectionString))
.applyToConnectionPoolSettings(builder -> builder
.maxSize(100)
.minSize(10)
.maxWaitTimeMillis(120000))
.applyToSocketSettings(builder -> builder
.connectTimeout(10000, java.util.concurrent.TimeUnit.MILLISECONDS))
.build();
return MongoClients.create(settings);
}
/**
* 配置事务管理器(MongoDB 4.0+ 支持事务)
*/
@Bean
public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
/**
* 移除 _class 字段
*/
@Bean
@Override
public MappingMongoConverter mappingMongoConverter(
MongoDatabaseFactory databaseFactory,
MongoCustomConversions customConversions,
MongoMappingContext mappingContext) {
MappingMongoConverter converter = super.mappingMongoConverter(
databaseFactory, customConversions, mappingContext);
// 移除 _class 字段
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
return converter;
}
/**
* 自定义类型转换
*/
@Bean
@Override
public MongoCustomConversions customConversions() {
return new MongoCustomConversions(Arrays.asList(
// LocalDateTime 转 Date
new org.springframework.core.convert.converter.Converter<LocalDateTime, Date>() {
@Override
public Date convert(LocalDateTime source) {
return Date.from(source.atZone(ZoneId.systemDefault()).toInstant());
}
},
// Date 转 LocalDateTime
new org.springframework.core.convert.converter.Converter<Date, LocalDateTime>() {
@Override
public LocalDateTime convert(Date source) {
return source.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
}
}
));
}
/**
* 启用索引自动创建
*/
@Override
protected boolean autoIndexCreation() {
return true;
}
}
7.2 审计配置
package com.example.mongodbdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Optional;
/**
* MongoDB审计配置
*/
@Configuration
@EnableMongoAuditing // 启用审计功能
public class MongoAuditConfig {
/**
* 审计员信息提供者
*/
@Bean
public AuditorAware<String> auditorProvider() {
return () -> {
// 从Spring Security获取当前用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.of("system");
}
return Optional.ofNullable(authentication.getName());
};
}
}
7.3 序列化配置
package com.example.mongodbdemo.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.text.SimpleDateFormat;
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
// 注册Java 8时间模块
objectMapper.registerModule(new JavaTimeModule());
// 禁用日期转时间戳
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 设置日期格式
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// 忽略未知属性
objectMapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper;
}
}
八、异常处理
8.1 自定义异常
package com.example.mongodbdemo.exception;
/**
* 资源不存在异常
*/
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
public ResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* 验证异常
*/
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
public ValidationException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* 业务异常
*/
public class BusinessException extends RuntimeException {
private String errorCode;
public BusinessException(String message) {
super(message);
}
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
8.2 全局异常处理器
package com.example.mongodbdemo.handler;
import com.example.mongodbdemo.dto.ApiResponse;
import com.example.mongodbdemo.exception.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理资源不存在异常
*/
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(
ResourceNotFoundException ex, WebRequest request) {
log.error("Resource not found: {}", ex.getMessage());
ApiResponse<Void> response = ApiResponse.error(
ex.getMessage(),
"RESOURCE_NOT_FOUND"
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
/**
* 处理验证异常
*/
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ApiResponse<Void>> handleValidationException(
ValidationException ex, WebRequest request) {
log.error("Validation error: {}", ex.getMessage());
ApiResponse<Void> response = ApiResponse.error(
ex.getMessage(),
"VALIDATION_ERROR"
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusinessException(
BusinessException ex, WebRequest request) {
log.error("Business error: {}", ex.getMessage());
ApiResponse<Void> response = ApiResponse.error(
ex.getMessage(),
ex.getErrorCode()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
ApiResponse<Map<String, String>> response = ApiResponse.error(
"参数验证失败",
"VALIDATION_FAILED"
);
response.setData(errors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理约束违反异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleConstraintViolationException(
ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation -> {
String fieldName = violation.getPropertyPath().toString();
String errorMessage = violation.getMessage();
errors.put(fieldName, errorMessage);
});
ApiResponse<Map<String, String>> response = ApiResponse.error(
"参数约束违反",
"CONSTRAINT_VIOLATION"
);
response.setData(errors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理唯一键冲突异常
*/
@ExceptionHandler(DuplicateKeyException.class)
public ResponseEntity<ApiResponse<Void>> handleDuplicateKeyException(
DuplicateKeyException ex) {
log.error("Duplicate key error: {}", ex.getMessage());
ApiResponse<Void> response = ApiResponse.error(
"数据已存在,请勿重复添加",
"DUPLICATE_KEY"
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}
/**
* 处理数据访问异常
*/
@ExceptionHandler(DataAccessException.class)
public ResponseEntity<ApiResponse<Void>> handleDataAccessException(
DataAccessException ex) {
log.error("Data access error: {}", ex.getMessage(), ex);
ApiResponse<Void> response = ApiResponse.error(
"数据访问异常,请稍后重试",
"DATA_ACCESS_ERROR"
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
/**
* 处理其他所有异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGlobalException(
Exception ex, WebRequest request) {
log.error("Unexpected error: {}", ex.getMessage(), ex);
ApiResponse<Void> response = ApiResponse.error(
"服务器内部错误,请稍后重试",
"INTERNAL_SERVER_ERROR"
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
九、测试
9.1 单元测试
package com.example.mongodbdemo.service;
import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
private User testUser;
@BeforeEach
void setUp() {
testUser = User.builder()
.id("123")
.firstName("John")
.lastName("Doe")
.email("john.doe@example.com")
.age(25)
.city("New York")
.status("ACTIVE")
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
}
@Test
void testCreateUser_Success() {
// Arrange
when(userRepository.existsByEmail(any())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(testUser);
// Act
User createdUser = userService.createUser(testUser);
// Assert
assertNotNull(createdUser);
assertEquals("John", createdUser.getFirstName());
assertEquals("Doe", createdUser.getLastName());
assertEquals("john.doe@example.com", createdUser.getEmail());
verify(userRepository).existsByEmail("john.doe@example.com");
verify(userRepository).save(any(User.class));
}
@Test
void testCreateUser_EmailExists() {
// Arrange
when(userRepository.existsByEmail(any())).thenReturn(true);
// Act & Assert
assertThrows(RuntimeException.class, () -> {
userService.createUser(testUser);
});
verify(userRepository).existsByEmail("john.doe@example.com");
verify(userRepository, never()).save(any(User.class));
}
@Test
void testGetUserById_Found() {
// Arrange
when(userRepository.findById("123")).thenReturn(Optional.of(testUser));
// Act
Optional<User> result = userService.getUserById("123");
// Assert
assertTrue(result.isPresent());
assertEquals("John", result.get().getFirstName());
verify(userRepository).findById("123");
}
@Test
void testGetUserById_NotFound() {
// Arrange
when(userRepository.findById("999")).thenReturn(Optional.empty());
// Act
Optional<User> result = userService.getUserById("999");
// Assert
assertFalse(result.isPresent());
verify(userRepository).findById("999");
}
@Test
void testUpdateUser_Success() {
// Arrange
User updatedUser = User.builder()
.firstName("Jane")
.lastName("Smith")
.age(26)
.build();
when(userRepository.findById("123")).thenReturn(Optional.of(testUser));
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setFirstName("Jane");
user.setLastName("Smith");
user.setAge(26);
return user;
});
// Act
User result = userService.updateUser("123", updatedUser);
// Assert
assertEquals("Jane", result.getFirstName());
assertEquals("Smith", result.getLastName());
assertEquals(26, result.getAge());
assertNotNull(result.getUpdatedAt());
verify(userRepository).findById("123");
verify(userRepository).save(any(User.class));
}
@Test
void testDeleteUser_Success() {
// Arrange
when(userRepository.existsById("123")).thenReturn(true);
doNothing().when(userRepository).deleteById("123");
// Act
userService.deleteUser("123");
// Assert
verify(userRepository).existsById("123");
verify(userRepository).deleteById("123");
}
@Test
void testDeleteUser_NotFound() {
// Arrange
when(userRepository.existsById("999")).thenReturn(false);
// Act & Assert
assertThrows(RuntimeException.class, () -> {
userService.deleteUser("999");
});
verify(userRepository).existsById("999");
verify(userRepository, never()).deleteById(any());
}
}
9.2 集成测试
package com.example.mongodbdemo.integration;
import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.ActiveProfiles;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@DataMongoTest
@ActiveProfiles("test")
class UserRepositoryIntegrationTest {
@Autowired
private UserRepository userRepository;
private User user1;
private User user2;
private User user3;
@BeforeEach
void setUp() {
// 清理数据
userRepository.deleteAll();
// 准备测试数据
user1 = User.builder()
.firstName("John")
.lastName("Doe")
.email("john.doe@example.com")
.age(25)
.city("New York")
.status("ACTIVE")
.registrationDate(LocalDateTime.now())
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
user2 = User.builder()
.firstName("Jane")
.lastName("Smith")
.email("jane.smith@example.com")
.age(30)
.city("Los Angeles")
.status("ACTIVE")
.registrationDate(LocalDateTime.now().minusDays(1))
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
user3 = User.builder()
.firstName("Bob")
.lastName("Johnson")
.email("bob.johnson@example.com")
.age(35)
.city("New York")
.status("INACTIVE")
.registrationDate(LocalDateTime.now().minusDays(2))
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
userRepository.saveAll(Arrays.asList(user1, user2, user3));
}
@AfterEach
void tearDown() {
userRepository.deleteAll();
}
@Test
void testSaveAndFindById() {
// Arrange
User newUser = User.builder()
.firstName("Alice")
.lastName("Brown")
.email("alice.brown@example.com")
.age(28)
.city("Chicago")
.status("ACTIVE")
.build();
// Act
User savedUser = userRepository.save(newUser);
Optional<User> foundUser = userRepository.findById(savedUser.getId());
// Assert
assertTrue(foundUser.isPresent());
assertEquals("Alice", foundUser.get().getFirstName());
assertEquals("Brown", foundUser.get().getLastName());
assertEquals("alice.brown@example.com", foundUser.get().getEmail());
}
@Test
void testFindByEmail() {
// Act
Optional<User> result = userRepository.findByEmail("john.doe@example.com");
// Assert
assertTrue(result.isPresent());
assertEquals("John", result.get().getFirstName());
assertEquals("Doe", result.get().getLastName());
}
@Test
void testFindByCity() {
// Act
List<User> users = userRepository.findByCity("New York");
// Assert
assertEquals(2, users.size());
assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("John")));
assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Bob")));
}
@Test
void testFindByAgeGreaterThan() {
// Act
List<User> users = userRepository.findByAgeGreaterThan(28);
// Assert
assertEquals(2, users.size());
assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Jane")));
assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Bob")));
}
@Test
void testFindByStatus() {
// Act
Pageable pageable = PageRequest.of(0, 10);
Page<User> activeUsers = userRepository.findByStatus("ACTIVE", pageable);
// Assert
assertEquals(2, activeUsers.getTotalElements());
assertEquals(2, activeUsers.getContent().size());
}
@Test
void testExistsByEmail() {
// Act
boolean exists = userRepository.existsByEmail("john.doe@example.com");
boolean notExists = userRepository.existsByEmail("nonexistent@example.com");
// Assert
assertTrue(exists);
assertFalse(notExists);
}
@Test
void testCountByCity() {
// Act
Long count = userRepository.countByCity("New York");
// Assert
assertEquals(2, count);
}
@Test
void testFindUsersByAgeRange() {
// Act
List<User> users = userRepository.findUsersByAgeRange(25, 35);
// Assert
assertEquals(2, users.size());
assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("John")));
assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Jane")));
}
@Test
void testDeleteByEmail() {
// Act
userRepository.deleteByEmail("john.doe@example.com");
// Assert
Optional<User> deletedUser = userRepository.findByEmail("john.doe@example.com");
assertFalse(deletedUser.isPresent());
}
}
十、应用启动和配置
10.1 主启动类
package com.example.mongodbdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableMongoAuditing // 启用审计功能
@EnableAsync // 启用异步支持
@EnableScheduling // 启用定时任务
@EnableConfigurationProperties // 启用配置属性
public class MongodbDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MongodbDemoApplication.class, args);
}
}
10.2 初始化数据
package com.example.mongodbdemo.init;
import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import java.time.LocalDateTime;
import java.util.Arrays;
/**
* 初始化数据(仅用于开发环境)
*/
@Configuration
@RequiredArgsConstructor
@Slf4j
@Profile("dev") // 只在开发环境运行
public class DataInitializer {
private final UserRepository userRepository;
@Bean
public CommandLineRunner initData() {
return args -> {
// 清理现有数据
userRepository.deleteAll();
// 创建测试用户
User admin = User.builder()
.firstName("Admin")
.lastName("User")
.email("admin@example.com")
.age(30)
.city("Beijing")
.country("China")
.status("ACTIVE")
.roles(Arrays.asList("ADMIN", "USER"))
.registrationDate(LocalDateTime.now())
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
User testUser = User.builder()
.firstName("Test")
.lastName("User")
.email("test@example.com")
.age(25)
.city("Shanghai")
.country("China")
.status("ACTIVE")
.roles(Arrays.asList("USER"))
.registrationDate(LocalDateTime.now())
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
userRepository.saveAll(Arrays.asList(admin, testUser));
log.info("Initialized {} users", userRepository.count());
};
}
}
十一、Swagger API文档
package com.example.mongodbdemo.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class SwaggerConfig {
@Value("${server.servlet.context-path:/api}")
private String contextPath;
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("MongoDB Demo API")
.description("Spring Boot MongoDB 集成示例")
.version("1.0.0")
.contact(new Contact()
.name("开发团队")
.email("dev@example.com"))
.license(new License()
.name("Apache 2.0")
.url("http://springdoc.org")))
.servers(List.of(
new Server().url(contextPath).description("API Server")
))
.components(new io.swagger.v3.oas.models.Components()
.addSecuritySchemes("bearer-key",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public")
.pathsToMatch("/api/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/api/admin/**")
.build();
}
}
十二、部署和配置建议
12.1 生产环境配置
# application-prod.yml
spring:
data:
mongodb:
uri: ${MONGODB_URI:mongodb://username:password@mongodb-host:27017/database?authSource=admin&replicaSet=rs0}
auto-index-creation: true
# 连接池配置
mongodb:
connection-pool:
max-size: 200
min-size: 20
max-wait-time: 300000
# 性能优化
server:
tomcat:
max-threads: 200
min-spare-threads: 20
# 监控
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
12.2 健康检查端点
package com.example.mongodbdemo.health;
import com.mongodb.client.MongoClient;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class MongoHealthIndicator implements HealthIndicator {
private final MongoTemplate mongoTemplate;
private final MongoClient mongoClient;
@Override
public Health health() {
try {
// 执行简单的MongoDB命令检查连接
mongoTemplate.executeCommand("{ ping: 1 }");
// 获取服务器状态
var serverStatus = mongoClient.getClusterDescription();
return Health.up()
.withDetail("clusterType", serverStatus.getType())
.withDetail("servers", serverStatus.getServerDescriptions().size())
.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
十三、性能优化建议
-
索引优化:
- 为常用查询字段创建索引
- 避免在频繁更新的字段上创建索引
- 使用复合索引覆盖查询
-
查询优化:
- 使用投影只返回需要的字段
- 避免在应用层进行大量数据处理
- 使用分页限制返回数据量
-
连接池优化:
- 根据并发量调整连接池大小
- 监控连接使用情况
-
监控和日志:
- 启用慢查询日志
- 监控MongoDB性能指标
- 使用Spring Boot Actuator

浙公网安备 33010602011771号