Spring框架太难学不会Java开发新手从环境配置到实战项目的完整学习指南轻松上手企业级开发
刚接触Spring的时候,我也是对着满屏的XML配置文件抓狂,看着那些注解不知道往哪放,感觉这玩意儿比解微积分方程还难。别慌,这篇文章就是写给当时那个迷茫的你的——我会用最直白的方式,带你一步步从环境配置到做出一个真正能跑的企业级项目。
为什么Spring让你头疼?先弄懂这些
很多人学Spring卡在第一步,不是因为你笨,而是你没搞清楚它到底在解决什么问题。
在Spring出现之前,Java开发的世界是这样的:你要手动创建对象、手动管理依赖、手动处理事务。想象一下,你写了一个用户管理系统,里面有好几个类互相依赖:
// 没有Spring的年代,你要这样写
public class UserController {
private UserService userService;
private UserRepository userRepository;
public UserController() {
// 你自己new出来,耦合死了
this.userService = new UserService();
this.userRepository = new UserRepository();
}
}
public class UserService {
private UserRepository userRepository;
public UserService() {
// 又是你自己new的
this.userRepository = new UserRepository();
}
}
这就像你每天出门都要自己造一辆车,而不是开车——你能开,但累得要死。
Spring的本质就是一个”超级工厂”,它帮你管理这些对象,你只需要告诉它”我想要什么”,它就给你”拿来什么”。这个管理对象的东西叫IoC(控制反转),它帮你把对象之间的关系管起来的东西叫DI(依赖注入)。记住这两个词,后面学Spring就少了一半困惑。
环境配置:一步一步来,别跳步
第一步:装JDK
Spring 5及以上版本需要Java 8+,建议你直接用Java 17,这是现在的LTS版本,很多新特性能让你的代码更简洁。
去oracle.com或者用OpenJDK都行。装完之后在终端验证:
java -version
javac -version
如果你看到类似这样的输出,就说明装好了:
openjdk version "17.0.8" 2023-07-18
OpenJDK Runtime Environment (build 17.0.8+0)
OpenJDK 64-Bit Server VM (build 17.0.8+0, mixed mode)
如果命令找不到,说明环境变量没配好。Windows用户需要在”系统属性”里把JAVA_HOME加上,然后把%JAVA_HOME%\bin加到Path里。Mac/Linux用户直接在~/.zshrc或~/.bashrc里加:
export JAVA_HOME=/usr/local/opt/openjdk@17
export PATH="$JAVA_HOME/bin:$PATH"
第二步:装IDEA
别再用Eclipse了,Spring Boot项目用IntelliJ IDEA社区版就够,如果公司有条件可以申请Ultimate版。IDEA对Spring的支持是碾压级的,注解提示、自动补全、调试功能都比其他IDE好太多。
装好之后,打开IDEA,点击”Configure” -> “Settings”,找到Plugins,搜索”Spring Assistant”安装。这不是必须的,但能让你创建Spring项目更方便。
第三步:创建第一个Spring Boot项目
这是最简单的开始方式。打开IDEA,点击”New Project”,选择”Spring Initializr”:
- Name:任意取,比如
spring-demo - Language:选Java
- Type:Maven
- Java:选17
- Group:
com.example - Artifact:
spring-demo - Dependencies:先勾这些:
- Spring Web
- Spring Data JPA
- MySQL Driver
- Lombok(后面会讲)
- Spring Boot DevTools
点击Finish,等IDEA自动下载依赖,第一次可能会慢一点,耐心等。
第一个Spring Boot项目跑起来
项目创建好后,你会看到一个SpringDemoApplication.java文件,里面有个main方法:
package com.example.springdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDemoApplication.class, args);
}
}
@SpringBootApplication是Spring Boot的启动注解,它包含了三个东西:
@Configuration:告诉Spring这是一个配置类@EnableAutoConfiguration:让Spring Boot自动配置@ComponentScan:扫描当前包下的所有组件
运行这个main方法,你会看到控制台输出:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.0)
Started SpringDemoApplication in 2.345 seconds
这时候服务器已经跑在8080端口了。打开浏览器访问http://localhost:8080,你会看到”Whitelabel Error Page”——别慌,这是正常的,因为我们还没写任何接口。
理解核心概念:IoC和DI
这是Spring最核心的两个概念,很多人在这一步就劝退了。其实没那么难。
IoC(控制反转):把对象的创建权交给Spring
没有Spring的时候,你要自己new对象:
public class OrderService {
private PaymentService paymentService;
public OrderService() {
// 你自己new,耦合严重
this.paymentService = new PaymentService();
}
}
有了Spring,你不用new了,Spring会帮你创建和注入:
@Service
public class OrderService {
// 不用构造函数new了,Spring会自动注入
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
@Service告诉Spring”我是一个服务类,请帮我管理”。@Autowired告诉Spring”请把PaymentService的实现类注入进来”。
DI(依赖注入):三种注入方式
Spring支持三种注入方式,我推荐你用构造器注入,这是最推荐的,也是Spring官方推荐的方式:
@Service
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
// 构造器注入,所有依赖都在构造函数里明确
@Autowired
public OrderService(PaymentService paymentService, InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
}
如果你用的是Java 16+,可以用record或者直接用Lombok的@RequiredArgsConstructor,代码更简洁:
@Service
@RequiredArgsConstructor
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
}
Controller层:写你的第一个接口
在com.example.springdemo包下新建一个controller包,里面创建UserController.java:
package com.example.springdemo.controller;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RequestMapping("/api/users")
public class UserController {
// 用Map模拟数据库存储,后面会换成真正的数据库
private final Map<Long, User> userStore = new ConcurrentHashMap<>();
private long idCounter = 1;
// 查询所有用户
@GetMapping
public List<User> getAllUsers() {
return new ArrayList<>(userStore.values());
}
// 根据ID查询
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userStore.get(id);
}
// 创建用户
@PostMapping
public User createUser(@RequestBody User user) {
user.setId(idCounter++);
userStore.put(user.getId(), user);
return user;
}
// 更新用户
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
User existingUser = userStore.get(id);
if (existingUser == null) {
throw new RuntimeException("用户不存在");
}
updatedUser.setId(id);
userStore.put(id, updatedUser);
return updatedUser;
}
// 删除用户
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
userStore.remove(id);
}
}
配套的User类:
package com.example.springdemo.controller;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String name;
private String email;
private Integer age;
}
运行项目,用Postman或者curl测试:
# 创建用户
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"张三","email":"zhangsan@example.com","age":25}'
# 查询所有用户
curl http://localhost:8080/api/users
# 查询单个用户
curl http://localhost:8080/api/users/1
Service层:业务逻辑的归宿
把业务逻辑从Controller抽出来,这是企业级开发的基本规范。在service包里创建UserService.java:
package com.example.springdemo.service;
import com.example.springdemo.entity.User;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Service
public class UserService {
private final Map<Long, User> userStore = new ConcurrentHashMap<>();
private long idCounter = 1;
public List<User> findAll() {
return new java.util.ArrayList<>(userStore.values());
}
public User findById(Long id) {
return userStore.get(id);
}
public User create(User user) {
user.setId(idCounter++);
userStore.put(user.getId(), user);
return user;
}
public User update(Long id, User updatedUser) {
User existingUser = userStore.get(id);
if (existingUser == null) {
throw new RuntimeException("用户ID " + id + " 不存在");
}
updatedUser.setId(id);
userStore.put(id, updatedUser);
return updatedUser;
}
public void delete(Long id) {
if (!userStore.containsKey(id)) {
throw new RuntimeException("用户ID " + id + " 不存在");
}
userStore.remove(id);
}
public List<User> findByAgeGreaterThan(int age) {
return userStore.values().stream()
.filter(u -> u.getAge() != null && u.getAge() > age)
.collect(Collectors.toList());
}
}
然后修改Controller,让它调用Service而不是直接操作Map:
package com.example.springdemo.controller;
import com.example.springdemo.entity.User;
import com.example.springdemo.service.UserService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
// 构造器注入Service
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping
public User createUser(@RequestBody User user) {
return userService.create(user);
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
return userService.update(id, user);
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
@GetMapping("/search/age/min/{age}")
public List<User> findUsersByMinAge(@PathVariable int age) {
return userService.findByAgeGreaterThan(age);
}
}
这样结构清晰了:Controller负责接收请求和返回响应,Service负责业务逻辑。后面如果你要加日志、缓存、事务管理,只需要在Service层加,不用改Controller。
数据库集成:Spring Data JPA上手
用内存存储只能做demo,真实项目肯定要用数据库。Spring Data JPA让数据库操作变得非常简单。
添加依赖
在pom.xml里确保有这些依赖:
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2内存数据库(开发阶段用) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MySQL驱动(生产环境用) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
配置数据库连接
在src/main/resources/application.properties里配置:
# 数据库连接
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
# 开启H2控制台
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
如果是用MySQL,改成这样:
spring.datasource.url=jdbc:mysql://localhost:3306/spring_demo?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=your_password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
创建实体类
package com.example.springdemo.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column
private Integer age;
}
创建Repository
package com.example.springdemo.repository;
import com.example.springdemo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 自定义查询方法,Spring Data JPA会自动实现
List<User> findByAgeGreaterThan(int age);
List<User> findByNameContaining(String keyword);
List<User> findByEmailEndingWith(String domain);
}
就是这么简单!你只需要定义接口,Spring会自动帮你实现CRUD操作。JpaRepository已经提供了:
save():保存或更新findById():根据ID查询findAll():查询所有deleteById():根据ID删除count():统计数量
重写Service层
package com.example.springdemo.service;
import com.example.springdemo.entity.User;
import com.example.springdemo.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> findAll() {
return userRepository.findAll();
}
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("用户不存在,ID: " + id));
}
@Transactional
public User create(User user) {
return userRepository.save(user);
}
@Transactional
public User update(Long id, User updatedUser) {
User existingUser = findById(id);
existingUser.setName(updatedUser.getName());
existingUser.setEmail(updatedUser.getEmail());
existingUser.setAge(updatedUser.getAge());
return userRepository.save(existingUser);
}
@Transactional
public void delete(Long id) {
if (!userRepository.existsById(id)) {
throw new RuntimeException("用户不存在,ID: " + id);
}
userRepository.deleteById(id);
}
public List<User> findByAgeGreaterThan(int age) {
return userRepository.findByAgeGreaterThan(age);
}
public List<User> searchByName(String keyword) {
return userRepository.findByNameContaining(keyword);
}
}
注意@Transactional注解,它告诉Spring这个方法需要事务管理。如果方法内任何地方抛出异常,整个事务会回滚,保证数据一致性。
异常处理:让API返回更友好
企业级项目不能有NullPointerException满天飞的情况。创建一个全局异常处理器:
package com.example.springdemo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException ex) {
Map<String, Object> body = Map.of(
"timestamp", LocalDateTime.now().toString(),
"status", HttpStatus.NOT_FOUND.value(),
"error", "Not Found",
"message", ex.getMessage()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGenericException(Exception ex) {
Map<String, Object> body = Map.of(
"timestamp", LocalDateTime.now().toString(),
"status", HttpStatus.INTERNAL_SERVER_ERROR.value(),
"error", "Internal Server Error",
"message", "服务器内部错误,请联系管理员"
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
这样当用户ID不存在时,你不会返回一堆堆栈跟踪,而是返回结构化的JSON错误信息。
分层架构:企业级项目的基本结构
一个规范的企业级Spring Boot项目应该是这样的:
src/main/java/com/example/demo/
├── DemoApplication.java # 启动类
├── config/ # 配置类
│ └── WebConfig.java
├── controller/ # 控制层
│ ├── UserController.java
│ └── OrderController.java
├── service/ # 服务层
│ ├── UserService.java
│ └── OrderService.java
├── repository/ # 数据访问层
│ ├── UserRepository.java
│ └── OrderRepository.java
├── entity/ # 实体类
│ ├── User.java
│ └── Order.java
├── dto/ # 数据传输对象
│ ├── UserCreateDTO.java
│ └── UserUpdateDTO.java
├── exception/ # 异常处理
│ ├── GlobalExceptionHandler.java
│ └── BusinessException.java
└── util/ # 工具类
└── DateUtil.java
DTO(数据传输对象)很重要,它防止数据库实体直接暴露给前端:
package com.example.demo.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserCreateDTO {
private String name;
private String email;
private Integer age;
}
Controller层接收DTO,转换成Entity再交给Service层处理,这样前端改字段不会影响数据库结构。
测试:别跳过这步
好的代码必须有测试。Spring Boot自带的测试框架非常好用:
package com.example.demo;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void testSaveAndFind() {
User user = new User();
user.setName("李四");
user.setEmail("lisi@test.com");
user.setAge(30);
User saved = userRepository.save(user);
assertThat(saved.getId()).isNotNull();
assertThat(saved.getName()).isEqualTo("李四");
User found = userRepository.findById(saved.getId()).orElse(null);
assertThat(found).isNotNull();
assertThat(found.getEmail()).isEqualTo("lisi@test.com");
}
}
常见坑和解决方案
1. 循环依赖
@Service
public class UserService {
// 错误:A依赖B,B又依赖A,Spring启动会报错
private final OrderService orderService;
}
@Service
public class OrderService {
private final UserService userService;
}
解决方式:把共同依赖抽到一个新Service里,或者用@Lazy注解延迟加载:
@Service
public class UserService {
@Lazy
private final OrderService orderService;
}
2. 事务没生效
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private OrderRepository orderRepository;
@Transactional
public void createOrderAndUser(User user, Order order) {
User savedUser = userRepository.save(user);
order.setUserId(savedUser.getId());
orderRepository.save(order);
// 如果这里抛出异常,两个操作都会回滚
}
}
注意:@Transactional只对外部调用生效。如果同一个类内部调用带@Transactional的方法,事务不会生效。这是Spring AOP的代理机制决定的。
3. 启动报错:”No qualifying bean of type”
这说明Spring找不到某个Bean。检查:
- 类上有没有加
@Component、@Service等注解 - 包扫描路径是否正确
- 依赖注入的方式对不对(构造器注入、字段注入、setter注入混用容易出问题)
实战:做一个完整的用户管理系统
现在把前面学的东西串起来,做一个完整的项目。
项目结构
src/main/java/com/example/usermanagement/
├── UserManagementApplication.java
├── controller/
│ └── UserController.java
├── service/
│ └── UserService.java
├── repository/
│ └── UserRepository.java
├── entity/
│ └── User.java
├── dto/
│ ├── UserCreateRequest.java
│ ├── UserUpdateRequest.java
│ └── ApiResponse.java
└── exception/
└── GlobalExceptionHandler.java
实体类
package com.example.usermanagement.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "姓名不能为空")
@Size(min = 2, max = 50)
private String name;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
@Column(unique = true)
private String email;
private Integer age;
private String phone;
@Column(updatable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
@PrePersist和@PreUpdate是JPA的生命周期回调,自动设置创建和更新时间。
Repository
package com.example.usermanagement.repository;
import com.example.usermanagement.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByNameContaining(String keyword);
List<User> findByAgeBetween(int minAge, int maxAge);
@Query("SELECT u FROM User u WHERE u.createdAt > :date")
List<User> findUsersCreatedAfter(java.time.LocalDateTime date);
}
Service层
package com.example.usermanagement.service;
import com.example.usermanagement.dto.UserCreateRequest;
import com.example.usermanagement.dto.UserUpdateRequest;
import com.example.usermanagement.entity.User;
import com.example.usermanagement.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> findAll() {
return userRepository.findAll();
}
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new com.example.usermanagement.exception.ResourceNotFoundException(
"用户不存在,ID: " + id));
}
@Transactional
public User create(UserCreateRequest request) {
// 检查邮箱是否已存在
userRepository.findByEmail(request.email())
.ifPresent(u -> {
throw new IllegalArgumentException("邮箱已被注册: " + request.email());
});
User user = new User();
user.setName(request.name());
user.setEmail(request.email());
user.setAge(request.age());
user.setPhone(request.phone());
return userRepository.save(user);
}
@Transactional
public User update(Long id, UserUpdateRequest request) {
User user = findById(id);
if (request.name() != null) {
user.setName(request.name());
}
if (request.email() != null) {
// 检查新邮箱是否已被其他用户使用
userRepository.findByEmail(request.email())
.filter(existing -> !existing.getId().equals(id))
.ifPresent(u -> {
throw new IllegalArgumentException("邮箱已被其他用户使用: " + request.email());
});
user.setEmail(request.email());
}
if (request.age() != null) {
user.setAge(request.age());
}
if (request.phone() != null) {
user.setPhone(request.phone());
}
return userRepository.save(user);
}
@Transactional
public void delete(Long id) {
User user = findById(id);
userRepository.delete(user);
}
public List<User> search(String keyword) {
return userRepository.findByNameContaining(keyword);
}
}
自定义异常
package com.example.usermanagement.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Controller层
package com.example.usermanagement.controller;
import com.example.usermanagement.dto.ApiResponse;
import com.example.usermanagement.dto.UserCreateRequest;
import com.example.usermanagement.dto.UserUpdateRequest;
import com.example.usermanagement.entity.User;
import com.example.usermanagement.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<ApiResponse<List<User>>> getAll() {
return ResponseEntity.ok(ApiResponse.success(userService.findAll()));
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getById(@PathVariable Long id) {
return ResponseEntity.ok(ApiResponse.success(userService.findById(id)));
}
@PostMapping
public ResponseEntity<ApiResponse<User>> create(@Valid @RequestBody UserCreateRequest request) {
User user = userService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(user));
}
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<User>> update(@PathVariable Long id,
@Valid @RequestBody UserUpdateRequest request) {
User user = userService.update(id, request);
return ResponseEntity.ok(ApiResponse.success(user));
}
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.ok(ApiResponse.success(null));
}
@GetMapping("/search")
public ResponseEntity<ApiResponse<List<User>>> search(@RequestParam String keyword) {
return ResponseEntity.ok(ApiResponse.success(userService.search(keyword)));
}
}
统一响应格式
package com.example.usermanagement.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "操作成功", data);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, message, null);
}
}
DTO类
package com.example.usermanagement.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record UserCreateRequest(
@NotBlank(message = "姓名不能为空")
@Size(min = 2, max = 50)
String name,
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
String email,
Integer age,
String phone
) {}
package com.example.usermanagement.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
public record UserUpdateRequest(
@Size(min = 2, max = 50)
String name,
@Email(message = "邮箱格式不正确")
String email,
Integer age,
String phone
) {}
注意这里用的是Java 14+的record特性,比写一个完整的类简洁很多。record自动提供构造器、getter、equals、hashCode和toString方法。
配置类:自定义Web MVC行为
有些时候你需要自定义Spring Boot的默认配置。创建一个WebConfig.java:
package com.example.usermanagement.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000") // 前端地址
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true);
}
}
这是跨域配置,前端开发时经常会遇到跨域问题,配置这个就解决了。
运行和调试
启动项目后,你可以:
- 访问
http://localhost:8080/h2-console查看H2数据库控制台 - 用Postman或curl测试接口
- 在IDEA里打断点调试
# 创建用户
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"张三","email":"zhangsan@example.com","age":25,"phone":"13800138000"}'
# 查询所有用户
curl http://localhost:8080/api/users
# 搜索用户
curl "http://localhost:8080/api/users/search?keyword=张"
进阶:加个分页功能
企业级项目查询列表几乎都需要分页。Spring Data JPA自带分页支持:
// Service层
public Page<User> findAll(Pageable pageable) {
return userRepository.findAll(pageable);
}
// Controller层
@GetMapping
public ResponseEntity<ApiResponse<Page<User>>> getAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
Page<User> users = userService.findAll(pageable);
return ResponseEntity.ok(ApiResponse.success(users));
}
前端收到的响应会是:
{
"success": true,
"message": "操作成功",
"data": {
"content": [...],
"totalPages": 5,
"totalElements": 42,
"size": 10,
"number": 0,
"first": true,
"last": false
}
}
总结:学Spring的最佳路径
我当年学Spring踩过的坑,总结成这几条经验:
- 不要一上来就啃Spring Framework的XML配置,直接从Spring Boot开始,理解概念后再回头看原理
- 动手比看视频重要,看懂了不代表会用了,必须自己敲代码
- 理解IoC和DI,这两个概念搞清楚了,后面学AOP、事务管理就轻松了
- 学会看报错信息,Spring的报错信息通常很详细,里面有你需要的线索
- 不要怕报错,每次报错都是在帮你理解Spring的工作原理
你现在看到的这个项目,只是一个起点。后面你可以加上:
- 用户认证(Spring Security)
- 日志管理(SLF4J + Logback)
- 单元测试(JUnit 5 + Mockito)
- 接口文档(SpringDoc OpenAPI)
- 缓存(Spring Cache + Redis)
- 消息队列(Spring Kafka / RabbitMQ)
一步一步来,先把基础打牢。Spring框架的设计哲学是”约定优于配置”,一旦你理解了它的设计思路,写起来会非常顺手。
有任何问题欢迎随时问我,写代码的过程就是不断解决问题的过程,别怕犯错,每次错误都在让你变得更强大。
