Spring框架入门指南从配置陷阱到实战项目详解Java开发必备技能与常见错误避坑教程
先说个真实发生的事。
我有个朋友,第一次学Spring Boot的时候,花了整整三天时间在一个@Autowired报错上折腾。最后发现问题是——他忘记在类上加@Component注解了。三天!就为了这么一个小毛病。
这篇指南就是帮你们省去这些冤枉时间的。
一、Spring到底是什么玩意儿
别被那些官方文档绕晕了。用最直白的话说:
Spring就是一个帮你管理对象之间关系的工具。
想象一下你在搭积木,每块积木(对象)都需要和其他积木连接。如果没有工具帮忙,你得手动找到每一块积木,把它们用胶水粘在一起。Spring就是那个帮你自动把积木拼好的智能平台。
它的核心思想叫IoC(控制反转)和DI(依赖注入)。听着挺高大上,其实就一件事:
以前是你自己
new对象,现在是Spring帮你创建和管理对象,你只管用。
举个栗子 🌰
没有Spring的时候:
public class UserService {
private UserDao userDao = new UserDao(); // 自己创建依赖
public void register(String username) {
userDao.insert(username);
}
}
有Spring之后:
@Service
public class UserService {
@Autowired
private UserDao userDao; // Spring自动帮你注入
public void register(String username) {
userDao.insert(username);
}
}
看见没?你不用管UserDao怎么来的,Spring会帮你搞定。
二、Spring Boot vs Spring:别搞混了
很多人一开始就懵了:Spring和Spring Boot到底啥区别?
用一句话总结:
Spring是框架,Spring Boot是让Spring更好用的脚手架。
| 特性 | Spring | Spring Boot |
|---|---|---|
| 配置复杂度 | 需要大量XML或JavaConfig配置 | 开箱即用,零配置或极少配置 |
| 启动时间 | 手动配置,较慢 | 自动配置,快速启动 |
| 内嵌服务器 | 需要手动部署到Tomcat等 | 内置Tomcat/Jetty,直接运行 |
| 适合场景 | 大型复杂项目 | 快速开发、微服务 |
如果你是新手,直接学Spring Boot就对了。别被那些老教程带跑偏。
三、第一个Spring Boot项目:手把手带你跑起来
3.1 创建项目
去Spring Initializr,按这个选:
- Project: Maven
- Language: Java
- Spring Boot: 最新稳定版(现在是3.x系列)
- Dependencies: Web、Spring Data JPA、MySQL Driver、Lombok
下载解压后,用IDEA打开,目录结构大概是这样的:
src/
├── main/
│ ├── java/
│ │ └── com/example/demo/
│ │ ├── DemoApplication.java // 启动类
│ │ ├── controller/
│ │ │ └── UserController.java
│ │ ├── service/
│ │ │ └── UserService.java
│ │ ├── repository/
│ │ │ └── UserRepository.java
│ │ └── model/
│ │ └── User.java
│ └── resources/
│ └── application.properties
└── test/
3.2 启动类
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
就这三行,Spring Boot就启动了。是不是简单到让你不敢相信?
3.3 写一个最简单的接口
User.java
@Data
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String email;
private LocalDateTime createTime;
}
UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
看到没?就一个接口,连实现都不用写,Spring Data JPA已经帮你搞定了增删改查。
UserService.java
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User createUser(String username, String email) {
User user = new User();
user.setUsername(username);
user.setEmail(email);
user.setCreateTime(LocalDateTime.now());
return userRepository.save(user);
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
}
UserController.java
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public User createUser(@RequestBody CreateUserRequest request) {
return userService.createUser(request.getUsername(), request.getEmail());
}
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
}
启动项目,访问http://localhost:8080/api/users,是不是有数据就返回了?
四、配置陷阱:这些坑99%的人都踩过
4.1 端口冲突:8080被占用了
启动时报这个错:
Port 8080 was already in use
原因:另一个Java程序占了8080端口。
解决方法(三选一):
- 找那个程序,把它关掉
- 修改当前项目的端口:
# application.properties
server.port=8081
- 临时用命令行指定:
java -jar your-app.jar --server.port=9090
4.2 @Autowired报红但运行没问题
IDE里@Autowired下面有红线,但程序能跑。
原因:Spring的工具依赖没装好。
解决方法:
- IDEA:安装Spring Assistant插件
- 或者:File → Settings → Plugins → 搜索”Spring”安装
4.3 中文乱码
username: ???
原因:项目编码不是UTF-8。
解决方法:
- IDEA设置:File → Settings → Editor → File Encodings,全部设为UTF-8
- 在
application.properties加一行:
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=UTF-8
4.4 Cross-Origin 跨域问题
浏览器控制台报错:
Access to XMLHttpRequest at 'http://localhost:8080/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy
原因:前后端分离开发时,前端域名和后端端口不同,浏览器拦截了请求。
解决方法:
创建配置类:
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}
4.5 Bean循环依赖
BeanCurrentlyInCreationException: Error creating bean with name 'xxx'
原因:A依赖B,B依赖A,形成死循环。
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB; // A依赖B
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA; // B依赖A ← 死循环了!
}
解决方法:
- 重构代码,打破循环依赖(推荐)
- 加
@Lazy注解:
@Service
public class ServiceB {
@Lazy
@Autowired
private ServiceA serviceA;
}
五、核心注解深度解析
5.1 四大组件注解
// 表示这是一个Spring管理的组件
@Component
public class MyComponent {}
// 专门用于Service层
@Service
public class UserService {}
// 专门用于Controller层
@RestController
public class UserController {}
// 专门用于数据访问层
@Repository
public class UserRepositoryImpl implements UserRepository {}
这四个注解本质上是一样的,只是语义不同,方便区分层次。
5.2 参数绑定注解
@RestController
public class UserController {
// 获取路径参数:/api/users/123
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
// 获取查询参数:/api/users?name=zhangsan
@GetMapping("/users")
public List<User> getUsers(@RequestParam String name) {
return userService.getUsersByName(name);
}
// 获取请求体:POST /api/users
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
// 获取请求头
@GetMapping("/info")
public String getInfo(@RequestHeader("Authorization") String token) {
return token;
}
}
5.3 事务管理注解
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryRepository inventoryRepository;
// 加了这个注解,方法失败会自动回滚
@Transactional(rollbackFor = Exception.class)
public void createOrder(Long productId, int quantity) {
// 扣库存
inventoryRepository.reduceStock(productId, quantity);
// 创建订单
orderRepository.save(new Order(productId, quantity));
// 如果上面任何一步报错,两个操作都会回滚
}
}
注意事项:
@Transactional只能加在public方法上- 同一个类内部调用带
@Transactional的方法,事务不生效(因为绕过了代理) - 一定要指定
rollbackFor,否则默认只回滚RuntimeException
六、实战项目:在线书店系统
咱们不搞虚的,来做一个完整的项目。
6.1 项目结构
bookstore/
├── src/main/java/com/example/bookstore/
│ ├── BookstoreApplication.java
│ ├── config/
│ │ ├── SecurityConfig.java
│ │ └── SwaggerConfig.java
│ ├── controller/
│ │ ├── BookController.java
│ │ ├── OrderController.java
│ │ └── UserController.java
│ ├── service/
│ │ ├── BookService.java
│ │ ├── OrderService.java
│ │ └── UserService.java
│ ├── repository/
│ │ ├── BookRepository.java
│ │ ├── OrderRepository.java
│ │ └── UserRepository.java
│ ├── model/
│ │ ├── Book.java
│ │ ├── Order.java
│ │ ├── OrderItem.java
│ │ └── User.java
│ ├── dto/
│ │ ├── BookDTO.java
│ │ ├── OrderRequest.java
│ │ └── LoginRequest.java
│ └── exception/
│ ├── GlobalExceptionHandler.java
│ └── BusinessException.java
├── src/main/resources/
│ ├── application.properties
│ └── schema.sql
└── pom.xml
6.2 实体类
Book.java
@Entity
@Table(name = "books")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String author;
@Column(nullable = false)
private BigDecimal price;
@Column(nullable = false)
private Integer stock;
private String description;
@Column(nullable = false, updatable = false)
private LocalDateTime createTime;
private LocalDateTime updateTime;
@PrePersist
public void prePersist() {
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.updateTime = LocalDateTime.now();
}
}
User.java
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String email;
private String role; // "USER" or "ADMIN"
@Column(nullable = false)
private LocalDateTime createTime;
}
Order.java
@Entity
@Table(name = "orders")
@Data
@NoArgsConstructor
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long userId;
@Column(nullable = false)
private BigDecimal totalAmount;
@Column(nullable = false)
private String status; // "PENDING", "PAID", "SHIPPED", "COMPLETED"
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> items = new ArrayList<>();
@Column(nullable = false)
private LocalDateTime createTime;
}
6.3 仓库层
public interface BookRepository extends JpaRepository<Book, Long> {
List<Book> findByTitleContaining(String keyword);
List<Book> findByAuthor(String author);
}
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByUserId(Long userId);
}
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);
}
6.4 服务层(重点来了)
BookService.java
@Service
@Slf4j
public class BookService {
@Autowired
private BookRepository bookRepository;
public Book createBook(BookDTO dto) {
Book book = new Book();
book.setTitle(dto.getTitle());
book.setAuthor(dto.getAuthor());
book.setPrice(dto.getPrice());
book.setStock(dto.getStock());
book.setDescription(dto.getDescription());
Book saved = bookRepository.save(book);
log.info("创建书籍成功,ID: {}", saved.getId());
return saved;
}
public Book updateBook(Long id, BookDTO dto) {
Book book = bookRepository.findById(id)
.orElseThrow(() -> new BusinessException("书籍不存在"));
book.setTitle(dto.getTitle());
book.setAuthor(dto.getAuthor());
book.setPrice(dto.getPrice());
book.setStock(dto.getStock());
book.setDescription(dto.getDescription());
return bookRepository.save(book);
}
public void deleteBook(Long id) {
if (!bookRepository.existsById(id)) {
throw new BusinessException("书籍不存在");
}
bookRepository.deleteById(id);
}
public Page<Book> listBooks(int page, int size, String keyword) {
Pageable pageable = PageRequest.of(page, size);
if (keyword != null && !keyword.isEmpty()) {
return bookRepository.findByTitleContaining(keyword, pageable);
}
return bookRepository.findAll(pageable);
}
}
OrderService.java(事务管理的典型应用)
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private BookRepository bookRepository;
@Autowired
private UserRepository userRepository;
@Transactional(rollbackFor = Exception.class)
public Order createOrder(Long userId, List<OrderItemDTO> items) {
// 1. 验证用户存在
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException("用户不存在"));
// 2. 计算总金额并扣库存
BigDecimal totalAmount = BigDecimal.ZERO;
Order order = new Order();
order.setUserId(userId);
order.setStatus("PENDING");
order.setCreateTime(LocalDateTime.now());
for (OrderItemDTO itemDTO : items) {
Book book = bookRepository.findById(itemDTO.getBookId())
.orElseThrow(() -> new BusinessException("书籍不存在: " + itemDTO.getBookId()));
if (book.getStock() < itemDTO.getQuantity()) {
throw new BusinessException("库存不足: " + book.getTitle());
}
// 扣库存
book.setStock(book.getStock() - itemDTO.getQuantity());
bookRepository.save(book);
// 计算金额
BigDecimal itemAmount = book.getPrice().multiply(BigDecimal.valueOf(itemDTO.getQuantity()));
totalAmount = totalAmount.add(itemAmount);
// 添加订单项
OrderItem orderItem = new OrderItem();
orderItem.setOrder(order);
orderItem.setBookId(book.getId());
orderItem.setQuantity(itemDTO.getQuantity());
orderItem.setPrice(book.getPrice());
order.getItems().add(orderItem);
}
order.setTotalAmount(totalAmount);
return orderRepository.save(order);
}
public Page<Order> listOrders(Long userId, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return orderRepository.findByUserId(userId, pageable);
}
}
6.5 控制器层
@RestController
@RequestMapping("/api/books")
@Tag(name = "书籍管理", description = "书籍相关API")
public class BookController {
@Autowired
private BookService bookService;
@PostMapping
@Operation(summary = "创建书籍")
public ResponseEntity<Book> createBook(@Valid @RequestBody BookDTO dto) {
return ResponseEntity.ok(bookService.createBook(dto));
}
@PutMapping("/{id}")
@Operation(summary = "更新书籍")
public ResponseEntity<Book> updateBook(@PathVariable Long id,
@Valid @RequestBody BookDTO dto) {
return ResponseEntity.ok(bookService.updateBook(id, dto));
}
@DeleteMapping("/{id}")
@Operation(summary = "删除书籍")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
return ResponseEntity.noContent().build();
}
@GetMapping
@Operation(summary = "查询书籍列表")
public ResponseEntity<Page<Book>> listBooks(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword) {
return ResponseEntity.ok(bookService.listBooks(page, size, keyword));
}
}
6.6 全局异常处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(ApiResponse.error(e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse> handleException(Exception e) {
log.error("系统异常", e);
return ResponseEntity.internalServerError()
.body(ApiResponse.error("系统错误,请稍后重试"));
}
}
6.7 配置文件
application.properties
# 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
# 服务器配置
server.port=8080
# 日志配置
logging.level.com.example.bookstore=DEBUG
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
七、测试:别忽略这块
很多新手不写测试,等你项目大了,改个地方就崩一片。
7.1 单元测试
@ExtendWith(MockitoExtension.class)
class BookServiceTest {
@Mock
private BookRepository bookRepository;
@InjectMocks
private BookService bookService;
@Test
void createBook_shouldSaveAndReturn() {
BookDTO dto = new BookDTO("Java编程思想", "Bruce Eckel", new BigDecimal("99"), 100, "经典教材");
Book savedBook = new Book(1L, "Java编程思想", "Bruce Eckel", new BigDecimal("99"), 100, "经典教材", LocalDateTime.now(), LocalDateTime.now());
when(bookRepository.save(any())).thenReturn(savedBook);
Book result = bookService.createBook(dto);
assertNotNull(result.getId());
assertEquals("Java编程思想", result.getTitle());
verify(bookRepository, times(1)).save(any());
}
}
7.2 集成测试
@SpringBootTest
@AutoConfigureMockMvc
class BookControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private BookRepository bookRepository;
@Test
void createBook_shouldReturn200() throws Exception {
BookDTO dto = new BookDTO("Spring Boot实战", "Craig Walls", new BigDecimal("89"), 50, "Spring入门");
mockMvc.perform(MockMvcRequestBuilders.post("/api/books")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("Spring Boot实战"));
}
}
八、性能优化:从菜鸟到进阶
8.1 缓存注解
@Service
public class BookService {
@Cacheable(value = "books", key = "#id")
public Book getBookById(Long id) {
// 第一次查询数据库,之后从缓存读取
return bookRepository.findById(id).orElseThrow(() -> new BusinessException("书籍不存在"));
}
@CacheEvict(value = "books", key = "#id")
public void deleteBook(Long id) {
bookRepository.deleteById(id);
}
@CacheEvict(value = "books", allEntries = true)
public void clearCache() {
// 清空所有书籍缓存
}
}
启动类加@EnableCaching:
@SpringBootApplication
@EnableCaching
public class BookstoreApplication {
public static void main(String[] args) {
SpringApplication.run(BookstoreApplication.class, args);
}
}
8.2 异步处理
@Service
public class NotificationService {
@Async
public void sendOrderNotification(Long orderId) {
// 异步发送通知,不阻塞主流程
log.info("发送订单通知: {}", orderId);
// 发送邮件/短信等
}
}
启动类加@EnableAsync。
8.3 数据库连接池配置
# HikariCP配置(Spring Boot默认)
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.connection-timeout=20000
九、部署与打包
9.1 打成Jar包
mvn clean package -DskipTests
生成target/bookstore-0.0.1-SNAPSHOT.jar
9.2 运行
java -jar bookstore-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
9.3 Docker部署
Dockerfile
FROM openjdk:17-slim
WORKDIR /app
COPY target/bookstore-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/bookstore
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=your_password
depends_on:
- db
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=your_password
- MYSQL_DATABASE=bookstore
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
十、常见错误速查表
| 错误信息 | 原因 | 解决方法 |
|---|---|---|
Field xxx in yyy required a bean of type 'xxx' that could not be found |
缺少组件注解 | 检查类上是否有@Component/@Service/@Repository |
BeanCurrentlyInCreationException |
循环依赖 | 使用@Lazy或重构代码 |
Transaction rolled back |
事务未正确配置 | 确保方法在@Transactional类中,且从外部调用 |
Cannot find bean |
包扫描路径不对 | 检查启动类的包路径,确保组件在同一包或子包下 |
404 Not Found |
路由配置问题 | 检查@RequestMapping路径是否匹配 |
Invalid bound statement |
MyBatis映射问题 | 检查Mapper接口和XML文件路径是否一致 |
Access denied |
权限问题 | 检查数据库用户名密码,或配置Spring Security |
十一、学习路线建议
第一阶段(1-2周)
- 理解IoC和DI概念
- 能独立创建Spring Boot项目
- 掌握基本注解的使用
第二阶段(2-4周)
- 学习Spring MVC,理解请求处理流程
- 掌握数据库操作(JPA/MyBatis)
- 学会使用Postman测试接口
第三阶段(1-2月)
- 学习Spring Security
- 掌握缓存、消息队列等高级特性
- 独立完成一个完整项目
第四阶段(持续)
- 学习微服务架构(Spring Cloud)
- 了解容器化部署
- 深入源码,理解Spring的设计思想
写在最后
Spring框架的学习曲线确实有点陡,但一旦你理解了它的核心思想,你会发现一切都很优雅。
记住几个关键点:
- 注解不是魔法,
@Autowired背后是Spring的依赖注入机制 - Bean的生命周期很重要,理解
@PostConstruct和@PreDestroy - 异常处理要统一,不要到处
try-catch - 测试不能省,单元测试和集成测试是你的安全带
遇到问题别慌,大多数错误都有现成的解决方案。Spring的社区非常活跃,stackoverflow上几乎你能想到的问题都有答案。
祝你在学习Spring的路上越走越顺!🚀
