目录
Spring Data JPA概述
Spring Data JPA优势
Spring Data JPA是Spring Data项目的一部分,基于JPA规范,为数据访问层提供了简化实现。
核心特性:
- Repository抽象:通过接口定义数据访问方法
- 查询方法生成:根据方法名自动生成查询
- 分页排序支持:内置分页和排序功能
- 事务管理:自动事务边界管理
- 审计功能:封装用户和时间信息
Maven依赖配置
<dependencies>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.7.5</version>
</dependency>
<!-- Hibernate Core -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.15.Final</version>
</dependency>
<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.5.1</version>
</dependency>
<!-- H2 Database (测试用) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>test</scope>
</dependency>
<!-- Testcontainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.17.6</version>
<scope>test</scope>
</dependency>
</dependencies>
Repository接口详解
基础Repository接口
实体类定义
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
@SequenceGenerator(name = "user_seq", sequenceName = "user_id_seq", allocationSize = 1)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(nullable = false)
private String password;
@Column(name = "first_name", length = 50)
private String firstName;
@Column(name = "last_name", length = 50)
private String lastName;
@Enumerated(EnumType.STRING)
@Column(name = "user_status", nullable = false)
private UserStatus status = UserStatus.ACTIVE;
@Enumerated(EnumType.STRING)
@Column(name = "user_role", nullable = false)
private UserRole role = UserRole.USER;
@CreatedDate
@Column(name = "created_date", nullable = false, updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
@Column(name = "last_modified_date")
private LocalDateTime lastModifiedDate;
@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;
@LastModifiedBy
@Column(name = "last_modified_by")
private String lastModifiedBy;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();
// 构造函数、getters、setters
}
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_number", unique = true, nullable = false)
private String orderNumber;
@Column(name = "total_amount", precision = 10, scale = 2)
private BigDecimal totalAmount;
@Enumerated(EnumType.STRING)
@Column(name = "order_status", nullable = false)
private OrderStatus status = OrderStatus.PENDING;
@Column(name = "order_date", nullable = false)
private LocalDateTime orderDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<OrderItem> items = new ArrayList<>();
// 构造函数、getters、setters
}
Repository接口层次
// 继承JpaRepository获得CRUD功能
public interface UserRepository extends JpaRepository<User, Long> {
// 根据用户名查找
Optional<User> findByUsername(String username);
// 根据邮箱查找
Optional<User> findByEmail(String email);
// 检查用户名是否存在
boolean existsByUsername(String username);
// 检查邮箱是否存在
boolean existsByEmail(String email);
// 根据状态查找用户
List<User> findByStatus(UserStatus status);
// 根据角色查找用户
List<User> findByRole(UserRole role);
// 根据用户名和状态查找
Optional<User> findByUsernameAndStatus(String username, UserStatus status);
// 查找用户名包含指定字符串的用户
List<User> findByUsernameContainingIgnoreCase(String username);
// 查找邮箱以指定域名结尾的用户
List<User> findByEmailEndingWith(String emailDomain);
// 根据创建日期范围查找用户
List<User> findByCreatedDateBetween(LocalDateTime startDate, LocalDateTime endDate);
// 统计不同状态的用户数量
@Query("SELECT u.status, COUNT(u) FROM User u GROUP BY u.status")
List<Object[]> countByStatus();
// 分页查询用户
Page<User> findByStatus(UserStatus status, Pageable pageable);
// 排序查询
List<User> findByOrderByCreatedDateDesc();
// 删除指定状态之前的用户
@Transactional
@Modifying
@Query("DELETE FROM User u WHERE u.createdDate < :cutoffDate")
int deleteOldUsers(@Param("cutoffDate") LocalDateTime cutoffDate);
}
查询方法命名规则
关键字查询模式
基础查询关键字
public interface ProductRepository extends JpaRepository<Product, Long> {
// FIND/GET 关键字查询
List<Product> findByCategory(String category);
Optional<Product> findByName(String name);
// EXISTS 存在性查询
boolean existsByName(String name);
boolean existsByCategoryIgnoreCase(String category);
// COUNT 计数查询
long countByCategory(String category);
long countByPriceGreaterThan(BigDecimal price);
// DELETE 删除查询
@Transactional
void deleteByCategory(String category);
// DISTINCT 去重查询
List<String> findDistinctCategoryByNameContaining(String name);
// TOP/LIMIT 限制结果数量
List<Product> findTop5ByOrderByPriceDesc();
List<Product> findFirst10ByCategoryOrderByNameAsc(string category);
// AND/OR 逻辑操作
List<Product> findByNameAndCategory(String name, String.category);
List<Product> findByNameOrCategory(String name, String category);
// BETWEEN 范围查询
List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
List<Product> findByCreatedDateBetween(LocalDateTime startDate, LocalDateTime endDate);
// COMPARISON 比较操作
List<Product> findByPriceGreaterThan(BigDecimal price);
List<Product> findByPriceGreaterThanEqual(BigDecimal price);
List<Product> findByPriceLessThan(BigDecimal price);
List<Product> findByPriceLessThanEqual(BigDecimal price);
List<Product> findByPriceGreaterThanAndPriceLessThan(BigDecimal minPrice, BigDecimal maxPrice);
// NULL 空值查询
List<Product> findByNameIsNull();
List<Product> findByCategoryIsNotNull();
// LIKE 模糊查询
List<Product> findByNameLike(String namePattern);
List<Product> findByNameNotLike(String namePattern);
// CONTAINING 包含查询
List<Product> findByNameContaining(String name);
List<Product> findByNameContainingIgnoreCase(String name);
// STARTS_WITH/ENDING_WITH 前缀/后缀查询
List<Product> findByNameStartsWith(String prefix);
List<Product> findByNameEndingWith(String suffix);
// REGEX 正则表达式查询
@Query("SELECT p FROM Product p WHERE REGEXP_LIKE(p.name, :pattern)")
List<Product> findByNameRegex(@Param("pattern") String pattern);
// ORDER_BY 排序查询
List<Product> findByCategoryOrderByNameAsc(String category);
List<Product> findByCategoryOrderByPriceDescNameAsc(String category);
// IN/NOT_IN 集合查询
List<Product> findByCategoryIn(List<String> categories);
List<Product> findByCategoryNotIn(List<String> categories);
}
复杂查询示例
public interface AdvancedRepository extends JpaRepository<Order, Long> {
// 多条件复合查询
List<Order> findByUserIdAndStatusAndOrderDateBetween(Long userId,
OrderStatus status,
LocalDateTime startDate,
LocalDateTime endDate);
// 金额范围查询
@Query("SELECT o FROM Order o WHERE o.totalAmount BETWEEN :minAmount AND :maxAmount")
List<Order> findByPriceRange(@Param("minAmount") BigDecimal minAmount,
@Param("maxAmount") BigDecimal maxAmount);
// 统计数据查询
@Query("SELECT COUNT(o) FROM Order o WHERE o.status = :status")
long countByStatus(@Param("status") OrderStatus status);
@Query("SELECT AVG(o.totalAmount) FROM Order o WHERE o.user.id = :userId")
BigDecimal findAverageAmountByUserId(@Param("userId") Long userId);
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.orderDate >= :startDate")
BigDecimal findTotalAmountSince(@Param("startDate") LocalDateTime startDate);
// 分组查询
@Query("SELECT o.status, COUNT(o), SUM(o.totalAmount) FROM Order o GROUP BY o.status")
List<Object[]> getOrderStatistics();
// 子查询
@Query("SELECT o FROM Order o WHERE o.user.id IN " +
"(SELECT u.id FROM User u WHERE u.status = :status)")
List<Order> findByUserStatus(@Param("status") UserStatus status);
// 复杂JOIN查询
@Query("SELECT o FROM Order o JOIN o.user u JOIN o.items i " +
"WHERE u.username = :username AND i.product.category = :category")
List<Order> findByUsernameAndProductCategory(@Param("username") String username,
@Param("category") String category);
}
自定义查询
@Query注解使用
JPQL查询
public interface CustomQueryRepository extends JpaRepository<User, Long> {
// JPQL查询示例
@Query("SELECT u FROM User u WHERE u.username = :username AND u.status = :status")
Optional<User> findByUsernameAndStatus(@Param("username") String username,
@Param("status") UserStatus status);
// 使用原生SQL查询
@Query(value = "SELECT * FROM users WHERE created_date >= ?1 AND status = ?2",
nativeQuery = true)
List<User> findRecentActiveUsers(LocalDateTime since, String status);
// 投影查询 - 只返回指定字段
@Query("SELECT new com.example.dto.UserSummaryDTO(u.id, u.username, u.email, " +
"u.createdDate, u.status) FROM User u WHERE u.status = :status")
List<UserSummaryDTO> findUserSummariesByStatus(@Param("status") UserStatus status);
// 聚合查询
@Query("SELECT COUNT(u), MIN(u.createdDate), MAX(u.createdDate) " +
"FROM User u WHERE u.status = :status")
Object[] getUserStatistics(@Param("status") UserStatus status);
// CASE WHEN查询
@Query("SELECT u, CASE WHEN u.createdDate >= :recentCutoff THEN 'RECENT' " +
"ELSE 'OLD' END FROM User u")
List<Object[]> classifyUsers(@Param("recentCutoff") LocalDateTime recentCutoff);
// 窗口函数查询(PostgreSQL特有)
@Query(value = "SELECT u.*, ROW_NUMBER() OVER (PARTITION BY status ORDER BY created_date) " +
"FROM users u", nativeQuery = true)
List<Object[]> findUsersWithRowNumber();
}
Specification动态查询
Specification实现
public class UserSpecifications {
public static Specification<User> hasUsername(String username) {
return (root, query, criteriaBuilder) ->
username == null ? null : criteriaBuilder.equal(root.get("username"), username);
}
public static Specification<User> hasStatus(UserStatus status) {
return (root, query, criteriaBuilder) ->
status == null ? null : criteriaBuilder.equal(root.get("status"), status);
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, criteriaBuilder) ->
date == null ? null : criteriaBuilder.greaterThan(root.get("createdDate"), date);
}
public static Specification<User> emailContaining(String email) {
return (root, query, criteriaBuilder) -> {
if (email == null || email.trim().isEmpty()) {
return null;
}
return criteriaBuilder.like(criteriaBuilder.lower(root.get("email")),
"%" + email.toLowerCase() + "%");
};
}
public static Specification<User> usernameLike(String namePattern) {
return (root, query, criteriaBuilder) -> {
if (namePattern == null || namePattern.trim().isEmpty()) {
return null;
}
return criteriaBuilder.or(
criteriaBuilder.like(criteriaBuilder.lower(root.get("username")),
"%" + namePattern.toLowerCase() + "%"),
criteriaBuilder.like(criteriaBuilder.lower(root.get("firstName")),
"%" + namePattern.toLowerCase() + "%"),
criteriaBuilder.like(criteriaBuilder.lower(root.get("lastName")),
"%" + namePattern.toLowerCase() + "%")
);
};
}
// 组合条件查询
public static Specification<User> buildSearchSpecification(UserSearchCriteria criteria) {
return Specification.where(hasUsername(criteria.getUsername()))
.and(hasStatus(criteria.getStatus()))
.and(createdAfter(criteria.getCreatedAfter()))
.and(emailContaining(criteria.getEmail()))
.and(usernameLike(criteria.getNamePattern()));
}
}
使用Specification进行动态查询
@Service
public class UserSearchService {
@Autowired
private UserRepository userRepository;
public Page<User> searchUsers(UserSearchCriteria criteria, Pageable pageable) {
Specification<User> spec = UserSpecifications.buildSearchSpecification(criteria);
return userRepository.findAll(spec, pageable);
}
public List<User> findUsersByComplexCriteria(UserSearchCriteria criteria) {
Specification<User> spec = Specification.where(
UserSpecifications.hasStatus(UserStatus.ACTIVE)
).and(
UserSpecifications.createdAfter(LocalDateTime.now().minusDays(30))
).and(
UserSpecifications.usernameLike(criteria.getNamePattern())
);
return userRepository.findAll(spec);
}
// 条件统计
public Map<String, Long> getUserStatistics(UserSearchCriteria criteria) {
Specification<User> spec = UserSpecifications.buildSearchSpecification(criteria);
List<User> users = userRepository.findAll(spec);
return users.stream()
.collect(Collectors.groupingBy(
user -> user.getStatus().name(),
Collectors.counting()
));
}
}
分页与排序
分页查询实现
基础分页配置
@Configuration
public class PaginationConfig {
@Bean
public PageRequest pageRequest() {
return PageRequest.of(0, 20);
}
}
分页查询服务
@Service
public class UserPaginationService {
@Autowired
private UserRepository userRepository;
// 基础分页查询
public Page<User> getAllUsers(Pageable pageable) {
return userRepository.findAll(pageable);
}
// 条件分页查询
public Page<User> findUsersByStatus(UserStatus status, Pageable pageable) {
return userRepository.findByStatus(status, pageable);
}
// 自定义排序分页
public Page<User> findUsersSortedBy(Sort sort) {
Pageable pageable = PageRequest.of(0, 20, sort);
return userRepository.findAll(pageable);
}
// 多字段排序
public Page<User> findUsersWithMultiSort() {
Sort sort = Sort.by(
Sort.Direction.DESC, "createdDate",
Sort.Direction.ASC, "username"
);
Pageable pageable = PageRequest.of(0, 20, sort);
return userRepository.findAll(pageable);
}
// 条件分页 + 自定义排序
public PagedModel<EntityModel<User>> findUsersPaged(UserSearchCriteria criteria,
Pageable pageable) {
Specification<User> spec = UserSpecifications.buildSearchSpecification(criteria);
Page<User> userPage = userRepository.findAll(spec, pageable);
// 使用Spring HATEOAS的PagedModel
return PagedModel.of(
userPage.getContent().stream()
.map(user -> EntityModel.of(user))
.collect(Collectors.toList()),
new PagedModel.PageMetadata(
userPage.getSize(),
userPage.getNumber(),
userPage.getTotalElements(),
userPage.getTotalPages()
)
);
}
// 分页查询统计信息
public UserPaginationSummary getUserPaginationSummary(Page<User> userPage) {
return UserPaginationSummary.builder()
.totalElements(userPage.getTotalElements())
.totalPages(userPage.getTotalPages())
.currentPage(userPage.getNumber())
.pageSize(userPage.getSize())
.first(userPage.isFirst())
.last(userPage.isLast())
.hasNext(userPage.hasNext())
.hasPrevious(userPage.hasPrevious())
.build();
}
}
分页查询控制器
@RestController
@RequestMapping("/api/users")
public class UserPaginationController {
@Autowired
private UserPaginationService userPaginationService;
@GetMapping
public ResponseEntity<PagedModel<EntityModel<User>>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdDate") String sortBy,
@RequestParam(defaultValue = "desc") String sortDir,
@RequestParam(required = false) String status,
@RequestParam(required = false) String search) {
// 构建排序条件
Sort.Direction direction = "desc".equalsIgnoreCase(sortDir) ?
Sort.Direction.DESC : Sort.Direction.ASC;
Sort sort = Sort.by(direction, sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
// 构建搜索条件
UserSearchCriteria criteria = UserSearchCriteria.builder()
.status(status != null ? UserStatus.valueOf(status.toUpperCase()) : null)
.namePattern(search)
.build();
PagedModel<EntityModel<User>> result = userPaginationService.findUsersPaged(criteria, pageable);
return ResponseEntity.ok(result);
}
@GetMapping("/statistics")
public ResponseEntity<Map<String, Object>> getUserStatistics() {
Map<String, Object> statistics = new HashMap<>();
// 总数统计
statistics.put("totalUsers", userRepository.count());
statistics.put("activeUsers", userRepository.countByStatus(UserStatus.ACTIVE));
// 分页统计
Page<User> recentUsers = userRepository.findByCreatedDateGreaterThan(
LocalDateTime.now().minusDays(30), PageRequest.of(0, 1));
statistics.put("totalPages", recentUsers.getTotalPages());
return ResponseEntity.ok(statistics);
}
}
事务管理
事务配置与使用
事务配置类
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
@Bean
public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
服务层事务应用
@Service
@Transactional(readOnly = true)
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private OrderRepository orderRepository;
// 只读事务
@Transactional(readOnly = true)
public List<User> findAllActiveUsers() {
return userRepository.findByStatus(UserStatus.ACTIVE);
}
// 写事务
@Transactional(rollbackFor = Exception.class)
public User createUserWithOrders(UserCreateRequest request) {
try {
// 创建用户
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPassword(encodePassword(request.getPassword()));
User savedUser = userRepository.save(user);
// 创建订单
if (request.getInitialOrders() != null) {
for (OrderCreateRequest orderRequest : request.getInitialOrders()) {
Order order = new Order();
order.setUser(savedUser);
order.setOrderNumber(generateOrderNumber());
order.setTotalAmount(orderRequest.getTotalAmount());
order.setStatus(OrderStatus.PENDING);
orderRepository.save(order);
}
}
return savedUser;
} catch (Exception e) {
// 事务会自动回滚
throw new BusinessException("用户创建失败", e);
}
}
// 嵌套事务
@Transactional
public User updateUserWithOrders(Long userId, UserUpdateRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
// 更新用户信息
updateUserBasicInfo(user, request);
// 处理订单更新(嵌套事务)
updateUserOrders(user, request.getOrderUpdates());
return userRepository.save(user);
}
@Transactional(propagation = Propagation.NESTED)
public void updateUserOrder(User user, OrderUpdateRequest request) {
Order order = orderRepository.findById(request.getOrderId()))
.orElseThrow(() -> new OrderNotFoundException(request.getOrderId()));
if (!order.getUser().equals(user)) {
throw new SecurityException("无权修改此订单");
}
order.setStatus(request.getStatus());
orderRepository.save(order);
// 模拟可能失败的操作
if (request.getStatus() == OrderStatus.CANCELLED) {
processOrderCancellation(order);
}
}
// 分布式事务(XA Transaction)
@Transactional
@TransactionalManager("transactionManager")
public void transferFunds(Long fromUserId, Long toUserId, BigDecimal amount) {
User fromUser = userRepository.findById(fromUserId)
.orElseThrow(() -> new UserNotFoundException(fromUserId));
User toUser = userRepository.findById(toUserId)
.orElseThrow(() -> new UserNotFoundException(toUserId));
// 扣款
fromUser.setBalance(fromUser.getBalance().subtract(amount));
if (fromUser.getBalance().compareTo(BigDecimal.ZERO) < 0) {
throw new InsufficientFundException("余额不足");
}
userRepository.save(fromUser);
// 加款
toUser.setBalance(toUser.getBalance().add(amount));
userRepository.save(toUser);
// 记录交易日志
auditLogService.logTransfer(fromUserId, toUserId, amount);
}
}
事务传播行为
传播行为示例
@Service
public class OrderProcessingService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
// REQUIRED:如果当前没有事务,创建新事务;如果有事务,加入事务
@Transactional(propagation = Propagation.REQUIRED)
public Order processOrder(OrderProcessRequest request) {
Order order = orderRepository.findById(request.getOrderId())
.orElseThrow(() -> new OrderNotFoundException(request.getOrderId()));
// 处理支付
PaymentResult payment = paymentService.processPayment(order);
// 更新库存
inventoryService.updateInventory(order);
// 更新订单状态
order.setStatus(OrderStatus.CONFIRMED);
return orderRepository.save(order);
}
// REQUIRES_NEW:总是创建新事务,独立于父事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrderAuditLog(Order order) {
OrderAuditLog log = new OrderAuditLog();
log.setOrderId(order.getId());
log.setAction("ORDER_CREATED");
log.setTimestamp(LocalDateTime.now());
// 即使主事务回滚,这个审计日志也会被保存
auditLogRepository.save(log);
}
// SUPPORTS:如果当前有事务,加入事务;如果没有事务,以非事务方式执行
@Transactional(propagation = Propagation.SUPPORTS)
public PaymentResult checkPaymentStatus(String paymentId) {
return paymentService.checkPayment(paymentId);
// 这不是关键操作,可以在没有事务的情况下执行
}
// NOT_SUPPORTED:以非事务方式执行,如果当前有事务则暂停
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendNotification(Order order) {
// 发送通知不应影响主事务
notificationService.sendOrderConfirmedNotification(order);
// 即使这里抛出异常,也不会影响主事务
}
// NEVER:以非事务方式执行,如果当前有事务则抛出异常
@Transactional(propagation = Propagation.NEVER)
public String generateReport(ReportRequest request) {
// 生成报告不应该在事务中执行
return reportService.generateOrderReport(request);
}
// MANDATORY:必须在事务中执行,如果没有事务则抛出异常
@Transactional(propagation = Propagation.MANDATORY)
public void updateCriticalData(CriticalData data) {
// 关键数据更新必须在事务中进行
criticalDataRepository.save(data);
}
}
总结
Spring Data JPA深入应用为企业级数据访问提供了强大的解决方案:
核心技术要点
- Repository抽象 - 简化数据访问层的开发
- 查询方法生成 - 基于方法名自动生成查询逻辑
- 自定义查询 - @Query注解和Specification动态查询
- 分页排序 - 内置的分页和排序功能支持
- 事务管理 - 灵活的事务控制机制
- 性能优化 - N+1问题解决和懒加载控制
- 实体关联 - 复杂对象关系的映射和管理
企业级应用价值
- 开发效率:通过Repository接口减少重复代码
- 代码质量:类型安全的查询方法定义
- 性能优化:智能的查询优化和缓存策略
- 维护性:清晰的数据访问层架构设计
最佳实践建议
- 合理使用懒加载:避免不必要的关联查询
- 批量操作优化:使用批量插入和更新提高性能
- 分页查询:大数据量查询必须使用分页
- 事务边界:合理控制事务的范围和传播行为
- 查询测试:对复杂查询进行充分的单元测试
继续深入学习Spring MVC的其他组件,将为您的Web应用开发提供更加完整和强大的技术基础!
1175

被折叠的 条评论
为什么被折叠?



